- 原文地址: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 語言程式設計師(比如我自己)來說前進了一大步。
所以在這篇文章中我將討論如何一步一步建立一個依賴 AWS Lambda 的 HTTPS API。我發現在這個過程中會有很多坑 — 特別是你對 AWS 的許可權系統不熟悉的話 — 而且 Lamdba 介面和其它 AWS 服務對接時有很多磕磕碰碰的地方。但是一旦你弄懂了,這些工具都會非常好使。
這篇教程涵蓋了許多方面的內容,所以我將它分成以下七個步驟:
通過這篇文章我們將努力構建一個具有兩個功能的 API:
方法 | 路徑 | 行為 |
---|---|---|
GET | /books?isbn=xxx | 展示帶有指定 ISBN 的 book 物件的資訊 |
POST | /books | 建立一個 book 物件 |
一個 book 物件是一條像這樣的原生 JSON 記錄:
{"isbn":"978-1420931693","title":"The Republic","author":"Plato"}
複製程式碼
我會保持 API 的簡單易懂,避免在特定功能的程式碼中陷入困境,但是當你掌握了基礎知識之後,怎樣擴充套件 API 來支援附加的路由和行為就變得輕而易舉了。
構建 AWS CLI
-
整個教程中我們會使用 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 和訪問私鑰。留意一下這些 —— 你將在下一步使用它們。 -
使用你剛建立的 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
地區 —— 如果你正在使用一個不同的地區,你需要相應地修改這個程式碼片段。)
建立並部署一個 Lambda 函式
-
接下來就是激動人心的時刻:建立一個 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 的嗎?
連結到 DynamoDB
-
在這一章中要為 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"} 複製程式碼
構建 HTTPS API
-
到現在為止,我們的 lambda 已經能夠執行並與 DynamoDB 互動。接下來就是建立一個通過 HTTPS 獲取 lamdba 函式的途徑,我們可以通過 AWS API 閘道器服務來實現。
但是在我們繼續之前,考慮一下專案的架構還是很有必要的。假設我們有一個巨集偉的計劃,我們的 lamdba 函式將是一個更大的
bookstore
API 的一部分,這個API 將會處理書本、客戶、推薦和其它各種各樣的資訊。AWS Lambda 提供了三種架構的基本選項:
- 微服務式 —— 每個 lambda 函式只響應一個行為。舉個例子,展示、建立和刪除一本書會對應 3 個獨立的 lambda 函式。
- 服務式 —— 每個 lambda 函式響應一組相關的行為。舉個例子, 用一個 lambda 來處理所有跟書相關的行為,但是使用者相關行為會被放到另一個獨立的 lambda 函式中。
- 整體式 —— 一個 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
-
既然 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。