[译] 使用 Go 和 AWS Lambda 构建无服务 API

早些时候 AWS 宣布了他们的 Lambda 服务将会为 Go 语言提供首要支持,这对于想要体验无服务技术的 GO 语言程序员(好比我本身)来讲前进了一大步。html

因此在这篇文章中我将讨论如何一步一步建立一个依赖 AWS Lambda 的 HTTPS API。我发如今这个过程当中会有不少坑 — 特别是你对 AWS 的权限系统不熟悉的话 — 并且 Lamdba 接口和其它 AWS 服务对接时有不少磕磕碰碰的地方。可是一旦你弄懂了,这些工具都会很是好使。前端

这篇教程涵盖了许多方面的内容,因此我将它分红如下七个步骤:linux

  1. 构建 AWS CLI
  2. 建立并部署一个 Lambda 函数
  3. 连接到 DynamoDB
  4. 构建 HTTPS API
  5. 处理事件
  6. 部署 API
  7. 支持多种行为

经过这篇文章咱们将努力构建一个具备两个功能的 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

  1. 整个教程中咱们会使用 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
    复制代码
  2. 接下来咱们须要建立一个带有容许程序访问权限的 AWS IAM 以供 CLI 使用。如何操做的指南能够在这儿找到。出于测试的目的,你能够为这个用户附加拥有全部权限的 AdministratorAccess 托管策略,但在实际生产中我建议你使用更严格的策略。建立完用户后你将得到一个访问密钥 ID 和访问私钥。留意一下这些 —— 你将在下一步使用它们。github

  3. 使用你刚建立的 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 函数

  1. 接下来就是激动人心的时刻:建立一个 lambda 函数。若是你正在照着作,进入你的 $GOPATH/src 文件夹,建立一个含有一个 main.go 文件的 books 仓库。

    $ cd ~/go/src
    $ mkdir books && cd books
    $ touch main.go
    复制代码
  2. 接着你须要安装 github.com/aws-lambda-go/lambda 包。这个包提供了建立 lambda 函数必需的 Go 语言库和类型。

    $ go get github.com/aws/aws-lambda-go/lambda
    复制代码
  3. 而后打开 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` 包构建(和解析)的对象。
复制代码
  1. 下一步是使用 go buildbooks 包构建一个可执行程序。在下面的代码片断中我使用 -o 标识来把可执行程序存到 /tmp/main ,固然,你也能够把它存到你想存的任意位置(一样地能够命名为任意名称)。

    $ env GOOS=linux GOARCH=amd64 go build -o /tmp/main books
    复制代码

    重要:做为这个命令的一部分,咱们使用 env 来设置两个命令运行期间的临时的环境变量(GOOS=linuxGOARCH=amd64)。这会指示 Go 编译器建立一个适用于 amd64 架构的 linux 系统的可执行程序 —— 就是当咱们部署到 AWS 上时将会运行的环境。

  2. AWS 要求咱们以 zip 格式上传 lambda 函数,因此建立一个包含咱们刚才建立的可执行程序的 main.zip 文件:

    $ zip -j /tmp/main.zip /tmp/main
    复制代码

须要注意的是可执行程序必须在 zip 文件的根目录下 —— 不是在 zip 文件的某个文件夹中。为了确保这一点,我在上面的代码片断中用了 -j 标识来丢弃目录名称。

  1. 下一步有点麻烦,可是对于让咱们的 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
    复制代码

    提示:你能够在这里找到一系列其余的许可政策,或许能对你有所帮助。

  2. 如今咱们能够真正地把 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"
        }
    }
    复制代码
  3. 大功告成!咱们的 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 的吗?

连接到 DynamoDB

  1. 在这一章中要为 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"
        }
    }
    复制代码
  2. 而后用 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"}}'
    复制代码
  3. 接下来更新咱们的 Go 代码,这样咱们的 lambda 处理程序能够链接并使用 DynamoDB 层。你须要安装 github.com/aws/aws-sdk-go 包,它提供了使用 DynamoDB(和其它 AWS 服务)的相关库。

    $ go get github.com/aws/aws-sdk-go
    复制代码
  4. 接着是敲代码环节。为了保持代码分离,在 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)
    }
    复制代码
  5. 保存文件、从新编译并打包压缩 lambda 函数,这样就作好了部署前的准备:

    $ env GOOS=linux GOARCH=amd64 go build -o /tmp/main books
    $ zip -j /tmp/main.zip /tmp/main
    复制代码
  6. 从新部署一个 lambda 函数比第一次建立轻松多了 —— 咱们能够像这样使用 aws lambda update-function-code 命令:

    $ aws lambda update-function-code --function-name books \
    --zip-file fileb:///tmp/main.zip
    复制代码
  7. 试着执行 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 的权限。咱们如今就把它改过来。

  8. 建立一个权限策略文件,给予 GetItemPutItem 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 有叫作 AWSLambdaDynamoDBExecutionRoleAWSLambdaInvocation-DynamoDB 的托管策略,听起来挺管用的,可是它们都不提供 GetItemPutItem 的权限。因此才须要组建本身的策略。

  9. 再执行一次 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"}
    复制代码

构建 HTTPS API

  1. 到如今为止,咱们的 lambda 已经可以运行并与 DynamoDB 交互。接下来就是创建一个经过 HTTPS 获取 lamdba 函数的途径,咱们能够经过 AWS API 网关服务来实现。

    可是在咱们继续以前,考虑一下项目的架构仍是颇有必要的。假设咱们有一个宏伟的计划,咱们的 lamdba 函数将是一个更大的 bookstore API 的一部分,这个API 将会处理书本、客户、推荐和其它各类各样的信息。

    AWS Lambda 提供了三种架构的基本选项:

    • 微服务式 —— 每一个 lambda 函数只响应一个行为。举个例子,展现、建立和删除一本书会对应 3 个独立的 lambda 函数。
    • 服务式 —— 每一个 lambda 函数响应一组相关的行为。举个例子, 用一个 lambda 来处理全部跟书相关的行为,可是用户相关行为会被放到另外一个独立的 lambda 函数中。
    • 总体式 —— 一个 lambda 函数管理书店的全部行为。

    每一个选项都是有效的,这里有一些关于每一个选项优缺点的不错的讨论。

    在这篇教程中咱们会用服务式进行操做,并用一个 books lambda 函数处理不一样的书本相关行为。这意味着咱们须要在咱们的 lambda 函数内部实现某种形式的路由,这一点我会在下文提到。不过如今……

  2. 咱们继续,使用 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 值,咱们在接下来几步中会屡次用到它。

  3. 接下来咱们须要获取 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 值。

  4. 如今咱们须要在根目录下建立一个新的资源 —— 就是 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+}

  5. 不过咱们不用这么作。咱们回到 /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
    }
    复制代码
  6. 如今万事俱备,就差把资源整合到咱们的 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": []
    }
    复制代码
  7. 好了,咱们来试一试。咱们可使用 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 函数的权限

  8. 最简单的修复问题的方法是使用 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 或其它更加容易说明的值。

  9. 好了,再试一次:

    $ 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 代码,而后作些转换了。

处理事件

  1. 提供 AWS API 网关须要的响应最简单的方法是安装 github.com/aws/aws-lambda-go/events 包:

    go get github.com/aws/aws-lambda-go/events
    复制代码

    这个包提供了许多有用的类型(APIGatewayProxyRequestAPIGatewayProxyResponse),包含了输入的 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"`
    }
    复制代码
  2. 回到 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 函数里包含它。

  1. 保存文件,从新编译并从新部署 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
    复制代码
  2. 再试一次,结果应该符合预期了。试试在查询字符串中输入不一样的 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
    }
    复制代码
  3. 插句题外话,全部发送到 os.Stderr 的信息会被打印到 AWS 云监控服务。因此若是你像上面的代码同样创建了一个错误日志器,你能够像这样在云监控上查询错误:

    $ aws logs filter-log-events --log-group-name /aws/lambda/books \
    --filter-pattern "ERROR"
    复制代码

部署 API

  1. 既然 API 可以正常工做了,是时候将它上线了。咱们能够执行这个 aws apigateway create-deployment 命令:

    $ aws apigateway create-deployment --rest-api-id rest-api-id \
    --stage-name staging
    {
        "id": "4pdblq",
        "createdDate": 1522929303
    }
    复制代码

    在上面的代码中我给 API 命名为 staging,你也能够按你的喜爱来给它起名。

  2. 部署之后你的 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
    复制代码

支持多种行为

  1. 咱们来为 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)
    }
    复制代码
  2. 从新编译、打包 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
    复制代码
  3. 如今当你用不一样的 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"}
    复制代码

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索