- 原文地址:How to build a Serverless API with Go and AWS Lambda
- 原文做者:Alex Edwards
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:sisibeloved
- 校对者:luochen1992、SergeyChang
早些时候 AWS 宣布了他们的 Lambda 服务将会为 Go 语言提供首要支持,这对于想要体验无服务技术的 GO 语言程序员(好比我本身)来讲前进了一大步。html
因此在这篇文章中我将讨论如何一步一步建立一个依赖 AWS Lambda 的 HTTPS API。我发如今这个过程当中会有不少坑 — 特别是你对 AWS 的权限系统不熟悉的话 — 并且 Lamdba 接口和其它 AWS 服务对接时有不少磕磕碰碰的地方。可是一旦你弄懂了,这些工具都会很是好使。前端
这篇教程涵盖了许多方面的内容,因此我将它分红如下七个步骤:linux
经过这篇文章咱们将努力构建一个具备两个功能的 API:android
方法 | 路径 | 行为 |
---|---|---|
GET | /books?isbn=xxx | 展现带有指定 ISBN 的 book 对象的信息 |
POST | /books | 建立一个 book 对象 |
一个 book 对象是一条像这样的原生 JSON 记录:ios
{"isbn":"978-1420931693","title":"The Republic","author":"Plato"}
复制代码
我会保持 API 的简单易懂,避免在特定功能的代码中陷入困境,可是当你掌握了基础知识以后,怎样扩展 API 来支持附加的路由和行为就变得垂手可得了。git
整个教程中咱们会使用 AWS CLI(命令行接口)来设置咱们的 lambda 函数和其它 AWS 服务。安装和基本使用指南能够在这儿找到,不过若是你使用了一个基于 Debian 的系统,好比 Ubuntu,你能够经过 apt
安装 CLI 并使用 aws
命令来运行它:程序员
$ sudo apt install awscli
$ aws --version
aws-cli/1.11.139 Python/3.6.3 Linux/4.13.0-37-generic botocore/1.6.6
复制代码
接下来咱们须要建立一个带有容许程序访问权限的 AWS IAM 以供 CLI 使用。如何操做的指南能够在这儿找到。出于测试的目的,你能够为这个用户附加拥有全部权限的 AdministratorAccess
托管策略,但在实际生产中我建议你使用更严格的策略。建立完用户后你将得到一个访问密钥 ID 和访问私钥。留意一下这些 —— 你将在下一步使用它们。github
使用你刚建立的 IAM 用户的凭证,经过 configure
命令来配置你的 CLI。你须要指定默认地区和你想要 CLI 使用的输出格式 。数据库
$ aws configure
AWS Access Key ID [None]: access-key-ID
AWS Secret Access Key [None]: secret-access-key
Default region name [None]: us-east-1
Default output format [None]: json
复制代码
(假定你使用的是 us-east-1
地区 —— 若是你正在使用一个不一样的地区,你须要相应地修改这个代码片断。)json
接下来就是激动人心的时刻:建立一个 lambda 函数。若是你正在照着作,进入你的 $GOPATH/src
文件夹,建立一个含有一个 main.go
文件的 books
仓库。
$ cd ~/go/src
$ mkdir books && cd books
$ touch main.go
复制代码
接着你须要安装 github.com/aws-lambda-go/lambda
包。这个包提供了建立 lambda 函数必需的 Go 语言库和类型。
$ go get github.com/aws/aws-lambda-go/lambda
复制代码
而后打开 main.go
文件,输入如下代码:
文件:books/main.go
package main
import (
"github.com/aws/aws-lambda-go/lambda"
)
type book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
}
func show() (*book, error) {
bk := &book{
ISBN: "978-1420931693",
Title: "The Republic",
Author: "Plato",
}
return bk, nil
}
func main() {
lambda.Start(show)
}
复制代码
在 main()
函数中咱们调用 lambda.Start()
并传入了 show
函数做为 lambda 处理程序。在这个示例中处理函数仅简单地初始化并返回了一个新的 book
对象。
Lamdba 处理程序可以接收一系列不一样的 Go 函数签名,并经过反射来肯定哪一个是你正在用的。它所支持的完整列表是……
```
func()
func() error
func(TIn) error
func() (TOut, error)
func(TIn) (TOut, error)
func(context.Context) error
func(context.Context, TIn) error
func(context.Context) (TOut, error)
func(context.Context, TIn) (TOut, error)
```
…… 其中的 `TIn` 和 `TOut` 参数是能够经过 Go 的 `encoding/json` 包构建(和解析)的对象。
复制代码
下一步是使用 go build
从 books
包构建一个可执行程序。在下面的代码片断中我使用 -o
标识来把可执行程序存到 /tmp/main
,固然,你也能够把它存到你想存的任意位置(一样地能够命名为任意名称)。
$ env GOOS=linux GOARCH=amd64 go build -o /tmp/main books
复制代码
重要:做为这个命令的一部分,咱们使用 env
来设置两个命令运行期间的临时的环境变量(GOOS=linux
和 GOARCH=amd64
)。这会指示 Go 编译器建立一个适用于 amd64 架构的 linux 系统的可执行程序 —— 就是当咱们部署到 AWS 上时将会运行的环境。
AWS 要求咱们以 zip 格式上传 lambda 函数,因此建立一个包含咱们刚才建立的可执行程序的 main.zip
文件:
$ zip -j /tmp/main.zip /tmp/main
复制代码
须要注意的是可执行程序必须在 zip 文件的根目录下 —— 不是在 zip 文件的某个文件夹中。为了确保这一点,我在上面的代码片断中用了 -j
标识来丢弃目录名称。
下一步有点麻烦,可是对于让咱们的 lambda 正确运行相当重要。咱们须要创建一个 IAM 角色,它定义了 lambda 函数运行时须要的权限。
如今让咱们来创建一个 lambda-books-executor
角色,并给它附加 AWSLambdaBasicExecutionRole
托管政策。这会给咱们的 lambda 函数运行和输出日志到 AWS 云监控服务所需的最基本的权限。
首先咱们须要建立一个信任策略 JSON 文件。这会从根本上指示 AWS 容许 lambda 服务扮演 lambda-books-executor
角色:
文件:/tmp/trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
复制代码
而后使用 aws iam create-role
命令来建立带有这个信任策略的用户:
$ aws iam create-role --role-name lambda-books-executor \
--assume-role-policy-document file:///tmp/trust-policy.json
{
"Role": {
"Path": "/",
"RoleName": "lambda-books-executor",
"RoleId": "AROAIWSQS2RVEWIMIHOR2",
"Arn": "arn:aws:iam::account-id:role/lambda-books-executor",
"CreateDate": "2018-04-05T10:22:32.567Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
}
}
复制代码
关注一下返回的 ARN(亚马逊资源名)—— 在下一步中你须要用到它。
如今这个 lambda-books-executor
已经被建立,咱们须要指定这个角色拥有的权限。最简单的方法是用 aws iam attach-role-policy
命令,像这样传入 AWSLambdaBasicExecutionRole
的 ARN 和许可政策:
$ aws iam attach-role-policy --role-name lambda-books-executor \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
复制代码
提示:你能够在这里找到一系列其余的许可政策,或许能对你有所帮助。
如今咱们能够真正地把 lambda 函数部署到 AWS 上了。咱们可使用 aws lambda create-function
命令。这个命令接收如下标识,而且须要运行一到两分钟。
--function-name |
将在 AWS 中被调用的 lambda 函数名 |
--runtime |
lambda 函数的运行环境(在咱们的例子里用 "go1.x" ) |
--role |
你想要 lambda 函数在运行时扮演的角色的 ARN(见上面的步骤 6) |
--handler |
zip 文件根目录下的可执行文件的名称 |
--zip-file |
zip 文件的路径 |
接下去尝试部署:
$ aws lambda create-function --function-name books --runtime go1.x \
--role arn:aws:iam::account-id:role/lambda-books-executor \
--handler main --zip-file fileb:///tmp/main.zip
{
"FunctionName": "books",
"FunctionArn": "arn:aws:lambda:us-east-1:account-id:function:books",
"Runtime": "go1.x",
"Role": "arn:aws:iam::account-id:role/lambda-books-executor",
"Handler": "main",
"CodeSize": 2791699,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2018-04-05T10:25:05.343+0000",
"CodeSha256": "O20RZcdJTVcpEiJiEwGL2bX1PtJ/GcdkusIEyeO9l+8=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
}
}
复制代码
大功告成!咱们的 lambda 函数已经被部署上去并能够用了。你可使用 aws lambda invoke
命令来试验一下(你须要为响应指定一个输出文件 —— 我在下面的代码片断中用了 /tmp/output.json
)。
$ aws lambda invoke --function-name books /tmp/output.json
{
"StatusCode": 200
}
$ cat /tmp/output.json
{"isbn":"978-1420931693","title":"The Republic","author":"Plato"}
复制代码
若是你一路照着作,你颇有可能获得一个相同的响应。注意到了咱们在 Go 代码中初始化的 book
对象是怎样被自动解析成 JSON 的吗?
在这一章中要为 lambda 函数存取的数据添加持久层。我将会使用 Amazon DynamoDB(它跟 AWS lambda 结合得很出色,而且免费用量也不小)。若是你对 DynamoDB 不熟悉,这儿有一个不错的基本纲要。
首先要建立一张 Books
表来保存 book 记录。DynanmoDB 是没有 schema 的,但咱们须要在 ISBN 字段上定义分区键(有点像主键)。咱们只需用如下这个命令:
$ aws dynamodb create-table --table-name Books \
--attribute-definitions AttributeName=ISBN,AttributeType=S \
--key-schema AttributeName=ISBN,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
{
"TableDescription": {
"AttributeDefinitions": [
{
"AttributeName": "ISBN",
"AttributeType": "S"
}
],
"TableName": "Books",
"KeySchema": [
{
"AttributeName": "ISBN",
"KeyType": "HASH"
}
],
"TableStatus": "CREATING",
"CreationDateTime": 1522924177.507,
"ProvisionedThroughput": {
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
},
"TableSizeBytes": 0,
"ItemCount": 0,
"TableArn": "arn:aws:dynamodb:us-east-1:account-id:table/Books"
}
}
复制代码
而后用 put-item
命令添加一些数据,这些数据在接下来几步中会用获得。
$ aws dynamodb put-item --table-name Books --item '{"ISBN": {"S": "978-1420931693"}, "Title": {"S": "The Republic"}, "Author": {"S": "Plato"}}'
$ aws dynamodb put-item --table-name Books --item '{"ISBN": {"S": "978-0486298238"}, "Title": {"S": "Meditations"}, "Author": {"S": "Marcus Aurelius"}}'
复制代码
接下来更新咱们的 Go 代码,这样咱们的 lambda 处理程序能够链接并使用 DynamoDB 层。你须要安装 github.com/aws/aws-sdk-go
包,它提供了使用 DynamoDB(和其它 AWS 服务)的相关库。
$ go get github.com/aws/aws-sdk-go
复制代码
接着是敲代码环节。为了保持代码分离,在 books
仓库中建立一个新的 db.go
文件:
$ touch ~/go/src/books/db.go
复制代码
并添加如下代码:
文件:books/db.go
package main
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
// 声明一个新的 DynamoDB 实例。注意它在并发调用时是
// 安全的。
var db = dynamodb.New(session.New(), aws.NewConfig().WithRegion("us-east-1"))
func getItem(isbn string) (*book, error) {
// 准备查询的输入
input := &dynamodb.GetItemInput{
TableName: aws.String("Books"),
Key: map[string]*dynamodb.AttributeValue{
"ISBN": {
S: aws.String(isbn),
},
},
}
// 从 DynamoDB 检索数据。若是没有符合的数据
// 返回 nil。
result, err := db.GetItem(input)
if err != nil {
return nil, err
}
if result.Item == nil {
return nil, nil
}
// 返回的 result.Item 对象具备隐含的
// map[string]*AttributeValue 类型。咱们可使用 UnmarshalMap helper
// 解析成对应的数据结构。注意:
// 当你须要处理多条数据时,可使用
// UnmarshalListOfMaps。
bk := new(book)
err = dynamodbattribute.UnmarshalMap(result.Item, bk)
if err != nil {
return nil, err
}
return bk, nil
}
复制代码
而后用新的代码更新 main.go
:
文件:books/main.go
package main
import (
"github.com/aws/aws-lambda-go/lambda"
)
type book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
}
func show() (*book, error) {
// 从 DynamoDB 数据库获取特定的 book 记录。在下一章中,
// 咱们可让这个行为更加动态。
bk, err := getItem("978-0486298238")
if err != nil {
return nil, err
}
return bk, nil
}
func main() {
lambda.Start(show)
}
复制代码
保存文件、从新编译并打包压缩 lambda 函数,这样就作好了部署前的准备:
$ env GOOS=linux GOARCH=amd64 go build -o /tmp/main books
$ zip -j /tmp/main.zip /tmp/main
复制代码
从新部署一个 lambda 函数比第一次建立轻松多了 —— 咱们能够像这样使用 aws lambda update-function-code
命令:
$ aws lambda update-function-code --function-name books \
--zip-file fileb:///tmp/main.zip
复制代码
试着执行 lambda 函数看看:
$ aws lambda invoke --function-name books /tmp/output.json
{
"StatusCode": 200,
"FunctionError": "Unhandled"
}
$ cat /tmp/output.json
{"errorMessage":"AccessDeniedException: User: arn:aws:sts::account-id:assumed-role/lambda-books-executor/books is not authorized to perform: dynamodb:GetItem on resource: arn:aws:dynamodb:us-east-1:account-id:table/Books\n\tstatus code: 400, request id: 2QSB5UUST6F0R3UDSVVVODTES3VV4KQNSO5AEMVJF66Q9ASUAAJG","errorType":"requestError"}
复制代码
啊,有点小问题。咱们能够从输出信息中看到,咱们的 lambda 函数(注意了,用的 lambda-books-executor
角色)缺乏在 DynamoDB 实例上运行 GetItem
的权限。咱们如今就把它改过来。
建立一个权限策略文件,给予 GetItem
和 PutItem
DynamoDB 相关的权限:
文件:/tmp/privilege-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
],
"Resource": "*"
}
]
}
复制代码
而后使用 aws iam put-role-policy
命令把它附加到 lambda-books-executor
用户:
$ aws iam put-role-policy --role-name lambda-books-executor \
--policy-name dynamodb-item-crud-role \
--policy-document file:///tmp/privilege-policy.json
复制代码
讲句题外话,AWS 有叫作 AWSLambdaDynamoDBExecutionRole
和 AWSLambdaInvocation-DynamoDB
的托管策略,听起来挺管用的,可是它们都不提供 GetItem
或 PutItem
的权限。因此才须要组建本身的策略。
再执行一次 lambda 函数看看。这一次应该顺利执行了并返回 ISBN 为 978-0486298238
的书本的信息:
$ aws lambda invoke --function-name books /tmp/output.json
{
"StatusCode": 200
}
$ cat /tmp/output.json
{"isbn":"978-0486298238","title":"Meditations","author":"Marcus Aurelius"}
复制代码
到如今为止,咱们的 lambda 已经可以运行并与 DynamoDB 交互。接下来就是创建一个经过 HTTPS 获取 lamdba 函数的途径,咱们能够经过 AWS API 网关服务来实现。
可是在咱们继续以前,考虑一下项目的架构仍是颇有必要的。假设咱们有一个宏伟的计划,咱们的 lamdba 函数将是一个更大的 bookstore
API 的一部分,这个API 将会处理书本、客户、推荐和其它各类各样的信息。
AWS Lambda 提供了三种架构的基本选项:
每一个选项都是有效的,这里有一些关于每一个选项优缺点的不错的讨论。
在这篇教程中咱们会用服务式进行操做,并用一个 books
lambda 函数处理不一样的书本相关行为。这意味着咱们须要在咱们的 lambda 函数内部实现某种形式的路由,这一点我会在下文提到。不过如今……
咱们继续,使用 aws apigateway create-rest-api
建立一个 bookstore
API:
$ aws apigateway create-rest-api --name bookstore
{
"id": "rest-api-id",
"name": "bookstore",
"createdDate": 1522926250
}
复制代码
记录下返回的 rest-api-id
值,咱们在接下来几步中会屡次用到它。
接下来咱们须要获取 API 根目录("/"
)的 id。咱们可使用 aws apigateway get-resources
命令来取得:
$ aws apigateway get-resources --rest-api-id rest-api-id
{
"items": [
{
"id": "root-path-id",
"path": "/"
}
]
}
复制代码
一样地,记录返回的 root-path-id
值。
如今咱们须要在根目录下建立一个新的资源 —— 就是 URL 路径 /books
对应的资源。咱们可使用带有 --path-part
参数的 aws apigateway create-resource
命令:
$ aws apigateway create-resource --rest-api-id rest-api-id \
--parent-id root-path-id --path-part books
{
"id": "resource-id",
"parentId": "root-path-id",
"pathPart": "books",
"path": "/books"
}
复制代码
一样地,记录返回的 resource-id
,下一步要用到。
值得一提的是,可使用大括号将部分路径包裹起来来在路径中包含占位符。举个例子,books/{id}
的 --path-part
参数将会匹配 /books/foo
和 /books/bar
的请求,而且 id
的值能够经过一个事件对象(下文会提到)在你的 lambda 函数中获取。你也能够在占位符后加上后缀 +
,使它变得贪婪。若是你想匹配任意路径的请求,一种常见的作法是使用参数 --path-part {proxy+}
。
不过咱们不用这么作。咱们回到 /books
资源,使用 aws apigateway put-method
命令来注册 ANY
的 HTTP 方法。这意味着咱们的 /books
将会响应全部请求,不论什么 HTTP 方法。
$ aws apigateway put-method --rest-api-id rest-api-id \
--resource-id resource-id --http-method ANY \
--authorization-type NONE
{
"httpMethod": "ANY",
"authorizationType": "NONE",
"apiKeyRequired": false
}
复制代码
如今万事俱备,就差把资源整合到咱们的 lambda 函数中了,这一步咱们使用 aws apigateway put-integration
命令。关于这个命令的一些参数须要简短地解释一下:
The --type
参数应该为 AWS_PROXY
。当使用这个值时,AWS API 网关会以 『事件』的形式将 HTTP 请求的信息发送到 lambda 函数。这也会自动将 lambda 函数的输出转化成 HTTP 响应。
--integration-http-method
参数必须为 POST
。不要把这个和你的 API 资源响应的 HTTP 方法混淆了。
--uri
参数须要遵照这样的格式:
arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/your-lambda-function-arn/invocations
复制代码
记住了这些之后,你的命令看起来应该是这样的:
$ aws apigateway put-integration --rest-api-id rest-api-id \
--resource-id resource-id --http-method ANY --type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:account-id:function:books/invocations
{
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:account-id:function:books/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"cacheNamespace": "qtdn5h",
"cacheKeyParameters": []
}
复制代码
好了,咱们来试一试。咱们可使用 aws apigateway test-invoke-method
命令来向咱们刚才创建的资源发送一个测试请求:
$ aws apigateway test-invoke-method --rest-api-id rest-api-id --resource-id resource-id --http-method "GET"
{
"status": 500,
"body": "{\"message\": \"Internal server error\"}",
"headers": {},
"log": "Execution log for request test-request\nThu Apr 05 11:07:54 UTC 2018 : Starting execution for request: test-invoke-request\nThu Apr 05 11:07:54 UTC 2018 : HTTP Method: GET, Resource Path: /books\nThu Apr 05 11:07:54 UTC 2018 : Method request path: {}[TRUNCATED]Thu Apr 05 11:07:54 UTC 2018 : Sending request to https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/arn:aws:lambda:us-east-1:account-id:function:books/invocations\nThu Apr 05 11:07:54 UTC 2018 : Execution failed due to configuration error: Invalid permissions on Lambda function\nThu Apr 05 11:07:54 UTC 2018 : Method completed with status: 500\n",
"latency": 39
}
复制代码
啊,没有成功。若是你浏览了输出的日志,你应该能够看出问题出在这儿:
Execution failed due to configuration error: Invalid permissions on Lambda function
这是由于咱们的 bookstore
API 网关没有执行 lambda 函数的权限。
最简单的修复问题的方法是使用 aws lambda add-permission
命令来给 API 调用的权限,像这样:
$ aws lambda add-permission --function-name books --statement-id a-GUID \
--action lambda:InvokeFunction --principal apigateway.amazonaws.com \
--source-arn arn:aws:execute-api:us-east-1:account-id:rest-api-id/*/*/*
{
"Statement": "{\"Sid\":\"6d658ce7-3899-4de2-bfd4-fefb939f731\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:us-east-1:account-id:function:books\",\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:us-east-1:account-id:rest-api-id/*/*/*\"}}}"
}
复制代码
注意,--statement-id
参数必须是一个全局惟一的标识符。它能够是一个 random ID 或其它更加容易说明的值。
好了,再试一次:
$ aws apigateway test-invoke-method --rest-api-id rest-api-id --resource-id resource-id --http-method "GET"
{
"status": 502,
"body": "{\"message\": \"Internal server error\"}",
"headers": {},
"log": "Execution log for request test-request\nThu Apr 05 11:12:53 UTC 2018 : Starting execution for request: test-invoke-request\nThu Apr 05 11:12:53 UTC 2018 : HTTP Method: GET, Resource Path: /books\nThu Apr 05 11:12:53 UTC 2018 : Method request path: {}\nThu Apr 05 11:12:53 UTC 2018 : Method request query string: {}\nThu Apr 05 11:12:53 UTC 2018 : Method request headers: {}\nThu Apr 05 11:12:53 UTC 2018 : Endpoint response body before transformations: {\"isbn\":\"978-0486298238\",\"title\":\"Meditations\",\"author\":\"Marcus Aurelius\"}\nThu Apr 05 11:12:53 UTC 2018 : Endpoint response headers: {X-Amz-Executed-Version=$LATEST, x-amzn-Remapped-Content-Length=0, Connection=keep-alive, x-amzn-RequestId=48d29098-38c2-11e8-ae15-f13b670c5483, Content-Length=74, Date=Thu, 05 Apr 2018 11:12:53 GMT, X-Amzn-Trace-Id=root=1-5ac604b5-cf29dd70cd08358f89853b96;sampled=0, Content-Type=application/json}\nThu Apr 05 11:12:53 UTC 2018 : Execution failed due to configuration error: Malformed Lambda proxy response\nThu Apr 05 11:12:53 UTC 2018 : Method completed with status: 502\n",
"latency": 211
}
复制代码
仍是报错,不过消息已经变了:
Execution failed due to configuration error: Malformed Lambda proxy response
若是你仔细看输出你会看到下列信息:
Endpoint response body before transformations: {\"isbn\":\"978-0486298238\",\"title\":\"Meditations\",\"author\":\"Marcus Aurelius\"}
这里有明确的过程。API 和 lambda 函数交互并收到了正确的响应(一个解析成 JSON 的 book
对象)。只是 AWS API 网关将响应当成了错误的格式。
这是由于,当你使用 API 网关的 lambda 代理集成,lambda 函数的返回值 必须 是这样的 JSON 格式:
{
"isBase64Encoded": true|false,
"statusCode": httpStatusCode,
"headers": { "headerName": "headerValue", ... },
"body": "..."
}
复制代码
是时候回头看看 Go 代码,而后作些转换了。
提供 AWS API 网关须要的响应最简单的方法是安装 github.com/aws/aws-lambda-go/events
包:
go get github.com/aws/aws-lambda-go/events
复制代码
这个包提供了许多有用的类型(APIGatewayProxyRequest
和 APIGatewayProxyResponse
),包含了输入的 HTTP 请求的信息并容许咱们构建 API 网关可以理解的响应.
type APIGatewayProxyRequest struct {
Resource string `json:"resource"` // API 网关中定义的资源路径
Path string `json:"path"` // 调用者的 url 路径
HTTPMethod string `json:"httpMethod"`
Headers map[string]string `json:"headers"`
QueryStringParameters map[string]string `json:"queryStringParameters"`
PathParameters map[string]string `json:"pathParameters"`
StageVariables map[string]string `json:"stageVariables"`
RequestContext APIGatewayProxyRequestContext `json:"requestContext"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
}
复制代码
type APIGatewayProxyResponse struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
}
复制代码
回到 main.go
文件,更新 lambda 处理程序,让它使用这样的函数签名:
func(events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
复制代码
总的来说,处理程序会接收一个包含了一串 HTTP 请求信息的 APIGatewayProxyRequest
对象,而后返回一个 APIGatewayProxyResponse
对象(能够被解析成适合 AWS API 网关的 JSON 响应)。
文件:books/main.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"regexp"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
var isbnRegexp = regexp.MustCompile(`[0-9]{3}\-[0-9]{10}`)
var errorLogger = log.New(os.Stderr, "ERROR ", log.Llongfile)
type book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
}
func show(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// 从请求中获取查询 `isbn` 的字符串参数
// 并校验。
isbn := req.QueryStringParameters["isbn"]
if !isbnRegexp.MatchString(isbn) {
return clientError(http.StatusBadRequest)
}
// 根据 isbn 值从数据库中取出 book 记录
bk, err := getItem(isbn)
if err != nil {
return serverError(err)
}
if bk == nil {
return clientError(http.StatusNotFound)
}
// APIGatewayProxyResponse.Body 域是个字符串,因此
// 咱们将 book 记录解析成 JSON。
js, err := json.Marshal(bk)
if err != nil {
return serverError(err)
}
// 返回一个响应,带有表明成功的 200 状态码和 JSON 格式的 book 记录
// 响应体。
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: string(js),
}, nil
}
// 添加一个用来处理错误的帮助函数。它会打印错误日志到 os.Stderr
// 并返回一个 AWS API 网关可以理解的 500 服务器内部错误
// 的响应。
func serverError(err error) (events.APIGatewayProxyResponse, error) {
errorLogger.Println(err.Error())
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: http.StatusText(http.StatusInternalServerError),
}, nil
}
// 加一个简单的帮助函数,用来发送和客户端错误相关的响应。
func clientError(status int) (events.APIGatewayProxyResponse, error) {
return events.APIGatewayProxyResponse{
StatusCode: status,
Body: http.StatusText(status),
}, nil
}
func main() {
lambda.Start(show)
}
复制代码
注意到为何咱们的 lambda 处理程序返回的全部 error
值变成了 nil
?咱们不得不这么作,由于 API 网关在和 lambda 代理集成插件结合使用时不接收 error
对象 (这些错误会再一次引发『响应残缺』错误)。因此咱们须要在 lambda 函数里本身管理错误,并返回合适的 HTTP 响应。其实 error
这个返回参数是多余的,可是为了保持正确的函数签名,咱们仍是要在 lambda 函数里包含它。
保存文件,从新编译并从新部署 lambda 函数:
$ env GOOS=linux GOARCH=amd64 go build -o /tmp/main books
$ zip -j /tmp/main.zip /tmp/main
$ aws lambda update-function-code --function-name books \
--zip-file fileb:///tmp/main.zip
复制代码
再试一次,结果应该符合预期了。试试在查询字符串中输入不一样的 isbn
值:
$ aws apigateway test-invoke-method --rest-api-id rest-api-id \
--resource-id resource-id --http-method "GET" \
--path-with-query-string "/books?isbn=978-1420931693"
{
"status": 200,
"body": "{\"isbn\":\"978-1420931693\",\"title\":\"The Republic\",\"author\":\"Plato\"}",
"headers": {
"X-Amzn-Trace-Id": "sampled=0;root=1-5ac60df0-0ea7a560337129d1fde588cd"
},
"log": [TRUNCATED],
"latency": 1232
}
$ aws apigateway test-invoke-method --rest-api-id rest-api-id \
--resource-id resource-id --http-method "GET" \
--path-with-query-string "/books?isbn=foobar"
{
"status": 400,
"body": "Bad Request",
"headers": {
"X-Amzn-Trace-Id": "sampled=0;root=1-5ac60e1c-72fad7cfa302fd32b0a6c702"
},
"log": [TRUNCATED],
"latency": 25
}
复制代码
插句题外话,全部发送到 os.Stderr
的信息会被打印到 AWS 云监控服务。因此若是你像上面的代码同样创建了一个错误日志器,你能够像这样在云监控上查询错误:
$ aws logs filter-log-events --log-group-name /aws/lambda/books \
--filter-pattern "ERROR"
复制代码
既然 API 可以正常工做了,是时候将它上线了。咱们能够执行这个 aws apigateway create-deployment
命令:
$ aws apigateway create-deployment --rest-api-id rest-api-id \
--stage-name staging
{
"id": "4pdblq",
"createdDate": 1522929303
}
复制代码
在上面的代码中我给 API 命名为 staging
,你也能够按你的喜爱来给它起名。
部署之后你的 API 能够经过 URL 被访问:
https://rest-api-id.execute-api.us-east-1.amazonaws.com/staging
复制代码
用 curl 来试一试。它的结果应该跟预想中同样:
$ curl https://rest-api-id.execute-api.us-east-1.amazonaws.com/staging/books?isbn=978-1420931693
{"isbn":"978-1420931693","title":"The Republic","author":"Plato"}
$ curl https://rest-api-id.execute-api.us-east-1.amazonaws.com/staging/books?isbn=foobar
Bad Request
复制代码
咱们来为 POST /books
行为添加支持。咱们但愿它能读取并校验一条新的 book 记录(从 JSON 格式的 HTTP 请求体中),而后把它添加到 DynamoDB 表中。
既然不一样的 AWS 服务已经联通,扩展咱们的 lambda 函数来支持附加的行为多是这个教程最简单的部分了,由于这能够仅经过 Go 代码实现。
首先更新 db.go
文件,添加一个 putItem
函数:
文件:books/db.go
package main
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
var db = dynamodb.New(session.New(), aws.NewConfig().WithRegion("us-east-1"))
func getItem(isbn string) (*book, error) {
input := &dynamodb.GetItemInput{
TableName: aws.String("Books"),
Key: map[string]*dynamodb.AttributeValue{
"ISBN": {
S: aws.String(isbn),
},
},
}
result, err := db.GetItem(input)
if err != nil {
return nil, err
}
if result.Item == nil {
return nil, nil
}
bk := new(book)
err = dynamodbattribute.UnmarshalMap(result.Item, bk)
if err != nil {
return nil, err
}
return bk, nil
}
// 添加一条 book 记录到 DynamoDB。
func putItem(bk *book) error {
input := &dynamodb.PutItemInput{
TableName: aws.String("Books"),
Item: map[string]*dynamodb.AttributeValue{
"ISBN": {
S: aws.String(bk.ISBN),
},
"Title": {
S: aws.String(bk.Title),
},
"Author": {
S: aws.String(bk.Author),
},
},
}
_, err := db.PutItem(input)
return err
}
复制代码
而后修改 main.go
函数,这样 lambda.Start()
方法会调用一个新的 router
函数,根据 HTTP 请求的方法决定哪一个行为被调用:
文件:books/main.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"regexp"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
var isbnRegexp = regexp.MustCompile(`[0-9]{3}\-[0-9]{10}`)
var errorLogger = log.New(os.Stderr, "ERROR ", log.Llongfile)
type book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
}
func router(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
switch req.HTTPMethod {
case "GET":
return show(req)
case "POST":
return create(req)
default:
return clientError(http.StatusMethodNotAllowed)
}
}
func show(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
isbn := req.QueryStringParameters["isbn"]
if !isbnRegexp.MatchString(isbn) {
return clientError(http.StatusBadRequest)
}
bk, err := getItem(isbn)
if err != nil {
return serverError(err)
}
if bk == nil {
return clientError(http.StatusNotFound)
}
js, err := json.Marshal(bk)
if err != nil {
return serverError(err)
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: string(js),
}, nil
}
func create(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
if req.Headers["Content-Type"] != "application/json" {
return clientError(http.StatusNotAcceptable)
}
bk := new(book)
err := json.Unmarshal([]byte(req.Body), bk)
if err != nil {
return clientError(http.StatusUnprocessableEntity)
}
if !isbnRegexp.MatchString(bk.ISBN) {
return clientError(http.StatusBadRequest)
}
if bk.Title == "" || bk.Author == "" {
return clientError(http.StatusBadRequest)
}
err = putItem(bk)
if err != nil {
return serverError(err)
}
return events.APIGatewayProxyResponse{
StatusCode: 201,
Headers: map[string]string{"Location": fmt.Sprintf("/books?isbn=%s", bk.ISBN)},
}, nil
}
func serverError(err error) (events.APIGatewayProxyResponse, error) {
errorLogger.Println(err.Error())
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: http.StatusText(http.StatusInternalServerError),
}, nil
}
func clientError(status int) (events.APIGatewayProxyResponse, error) {
return events.APIGatewayProxyResponse{
StatusCode: status,
Body: http.StatusText(status),
}, nil
}
func main() {
lambda.Start(router)
}
复制代码
从新编译、打包 lambda 函数,而后像日常同样部署它:
$ env GOOS=linux GOARCH=amd64 go build -o /tmp/main books
$ zip -j /tmp/main.zip /tmp/main
$ aws lambda update-function-code --function-name books \
--zip-file fileb:///tmp/main.zip
复制代码
如今当你用不一样的 HTTP 方法访问 API 时,它应该调用合适的方法:
$ curl -i -H "Content-Type: application/json" -X POST \
-d '{"isbn":"978-0141439587", "title":"Emma", "author": "Jane Austen"}' \
https://rest-api-id.execeast-1.amazonaws.com/staging/books
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 7
Connection: keep-alive
Date: Thu, 05 Apr 2018 14:55:34 GMT
x-amzn-RequestId: 64262aa3-38e1-11e8-825c-d7cfe4d1e7d0
x-amz-apigw-id: E33T1E3eIAMF9dw=
Location: /books?isbn=978-0141439587
X-Amzn-Trace-Id: sampled=0;root=1-5ac638e5-e806a84761839bc24e234c37
X-Cache: Miss from cloudfront
Via: 1.1 a22ee9ab15c998bce94f1f4d2a7792ee.cloudfront.net (CloudFront)
X-Amz-Cf-Id: wSef_GJ70YB2-0VSwhUTS9x-ATB1Yq8anWuzV_PRN98k9-DkD7FOAA==
$ curl https://rest-api-id.execute-api.us-east-1.amazonaws.com/staging/books?isbn=978-0141439587
{"isbn":"978-0141439587","title":"Emma","author":"Jane Austen"}
复制代码
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。