[譯] 使用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索