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