API GatewayのLambda連携をAWS CLIからやってみる
AWS CLI が 2015/10/26 リリースのバージョン 1.9.0 から API Gateway に対応しました。
API Gateway が発表されたのは 2015 年 7 月ですので、CLI のリリースまでに三ヶ月以上を要しました。 CLI 対応記念として、getting started ドキュメントにある次のチュートリアルを AWS CLI から実行してみます。
演習: Lambda 関数 - API Gateway を使用して、カスタム API を作成し、一連の AWS Lambda 関数に接続した後に、API から Lambda 関数を呼び出します。
AWS CLI は生の REST API をガシガシ叩くだけですので、よりアプリケーション開発者フレンドリーに API Gateway&Lambda で開発したい方は、素直に JAWS フレームワークなどをご検討ください。
チュートリアルのゴール
「MyDemoAPI」という API Gateway を用意し、パス(リソース)「/mydemoresource」に GET/POST メソッドを定義します。
GET メソッドは Lambda 関数「GetHelloWorld」を、 POST メソッドは Lambda 関数「GetHelloWithName」 を invoke します。
作成した API を test ステージにデプロイし 定義した GET/POST メソッドにリクエストします。
以下の流れで作業します。
- AWS CLI をインストールする
- API を作成する
- リソースを作成する
- Lambda 関数を作成する
- GET メソッドを作成してテストする
- POST メソッドを作成してテストする
- API をデプロイする
- API をテストする
- クリーンアップ
注意事項
API Gateway を AWS CLI から操作するとハマるかもしれない箇所を3点ほど先に述べます。
一つ目。API Gateway の REST API は Hypertext Application Language(HAL) がベースになっており、これまでのような XML ではなく JSON でやり取りします(HAL 自体は XML もサポートします)。 AWS CLI の HAL サポートはまだこなれていないので、細かい動作が胡散臭いです。 暖かく見守ってください(すでにバグを3つほど踏抜きました)。
二つ目。今回のチュートリアルでは CLIENT -> API Gateway -> Lambda と連携します。
CLIENT -> API Gateway のメソッド(--http-method
)によらず、API Gateway は POST メソッド(--integration-http-method
)で Lambda 関数を invoke します。
三つ目。API Gateway が Lambda を invoke できるように Lambda 関数のパーミッションを設定しましょう。 パーミッションを設定し忘れると "Execution failed due to configuration error: Invalid permissions on Lambda function" というエラーが発生します。 マネージメントコンソールから操作する際は、パーミッション設定の確認ダイアログが表示されます。
それでは本題に戻ります。
1. AWS CLI をインストールする
AWS CLI からコマンド実行するため、なにはともあれ AWS CLI をインストールします。 AWS CLI はパッケージ管理ツールの pip からインストールします。
$ pip install awscli --upgrade $ aws --version aws-cli/1.9.2 Python/2.7.10 Darwin/14.5.0 botocore/1.3.2
AWS CLI のバージョンが少なくとも 1.9.2 以上であることを確認してください。
API Gateway のサービス名は apigateway です。
$ aws apigateway help
を叩くと、大量のコマンド一覧のヘルプ画面が出てくるはずです。
$ aws apigateway help APIGATEWAY() APIGATEWAY() apigateway - Amazon API Gateway helps developers deliver robust, secure and scalable mobile and web application backends. Amazon API Gateway allows develop- ers to securely connect mobile and web applications to APIs that run on AWS Lambda, Amazon EC2, or other publicly addressable web services that are hosted outside of AWS. o create-api-key o create-base-path-mapping o create-deployment o create-domain-name o create-model o create-resource o create-rest-api o create-stage o delete-api-key o delete-base-path-mapping o delete-client-certificate o delete-deployment o delete-domain-name o delete-integration o delete-integration-response o delete-method o delete-method-response o delete-model o delete-resource o delete-rest-api o delete-stage o flush-stage-cache o generate-client-certificate o get-account o get-api-key o get-api-keys o get-base-path-mapping o get-base-path-mappings o get-client-certificate o get-client-certificates o get-deployment o get-deployments o get-domain-name o get-domain-names o get-integration o get-integration-response o get-method o get-method-response o get-model o get-model-template o get-models o get-resource o get-resources o get-rest-api o get-rest-apis o get-sdk o get-stage o get-stages o help o put-integration o put-integration-response o put-method o put-method-response o test-invoke-method o update-account o update-api-key o update-base-path-mapping o update-client-certificate o update-deployment o update-domain-name o update-integration o update-integration-response o update-method o update-method-response o update-model o update-resource o update-rest-api o update-stage APIGATEWAY()
help コマンドをのぞいても 67 個あります。賑やかですね。
コマンドリファレンスはこちらです http://docs.aws.amazon.com/cli/latest/reference/apigateway/index.html
2. API を作成する
API の作成は create-rest-api API を使います。 API 名は "MyDemoAPI" です。
$ aws apigateway create-rest-api --name MyDemoAPI --description "This is my API for demonstration purposes" { "id": "asdfqwer", "name": "MyDemoAPI", "description": "This is my API for demonstration purposes", "createdDate": 1446272225 } $ aws apigateway get-rest-apis # API 一覧を確認 { "items": [ { "description": "This is my API for demonstration purposes", "createdDate": 1446272225, "id": "asdfqwer", "name": "MyDemoAPI" } ] } $ REST_API_ID="asdfqwer"
なお name が重複していても作成可能です。
最終的に API は REST_API_ID.execute-api.REGION.amazonaws.com のホスト名でアクセスすることになります。
3. リソースを作成する
API 作成直後はルート(/)リソースしか存在しません。
$ aws apigateway get-resources --rest-api-id $REST_API_ID { "items": [ { "path": "/", "id": "xzw05h7r0h" } ] } $ ROOT_ID="xzw05h7r0h"
ルート直下にリソース mydemoresource を追加します。
リソースの作成は create-resource API を使います。
親となるリソースルートを --parent-id
で指定します。
$ aws apigateway create-resource --rest-api-id $REST_API_ID --parent-id $ROOT_ID --path-part "mydemoresource" { "path": "/mydemoresource", "pathPart": "mydemoresource", "id": "zzzzzz", "parentId": "xxxxxxxx" } $ aws apigateway get-resources --rest-api-id $REST_API_ID { "items": [ { "path": "/", "id": "xxxxxxxx" }, { "path": "/mydemoresource", "id": "zzzzzz", "pathPart": "mydemoresource", "parentId": "xxxxxxxx" } ] }
マネジメントコンソールと異なり、path
と pathPart
を個別に指定できないようです。
4. Lambda 関数を作成する
チュートリアルに従い、Lambda 関数を2つ(GetHelloWithName と GetHelloWithName)作成してください。
$ aws lambda list-functions { "Functions": [ { ... "FunctionName": "GetHelloWithName", "FunctionArn": "arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName", ... }, { ... "FunctionName": "GetHelloWorld", "FunctionArn": "arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld", ... }, ] }
FunctionArn
はあとで利用します。
5. GET メソッドを作成してテストする
リソース mydemoresource
に GET すると、Lambda 関数 GetHelloWorld
を呼び出すように GET メソッドを定義します。
マネージメントコンソールでは次の画面に相当します。
- Method Request(クライアントから API Gateway へのリクエスト)
- put-integration(API Gateway から Lambda へのリクエスト)
- put-integration-response(Lambda から API Gateway へのレスポンス)
- put-method-response(API Gateway からクライアントへのレスポンス)
という4項目をそれぞれ AWS CLI から定義します。
Black Blet Tech シリーズ Amazon API Gateway の P.49 から先も合わせてご確認ください。
Method Request(クライアントから API Gateway へのリクエスト)
リソース /mydemoresource に対して GET メソッドを用意します(--http-method GET
)。
認証なしでリクエストできるようにするため --authorization-type None
とします。
http://docs.aws.amazon.com/cli/latest/reference/apigateway/put-method.html
$ aws apigateway put-method \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --authorization-type NONE \ --no-api-key-required \ --request-parameters {} { "apiKeyRequired": false, "httpMethod": "GET", "authorizationType": "NONE", "requestParameters": {} }
put-integration(API Gateway から Lambda へのリクエスト)
- API Gateway から Lambda 関数へのリクエストを定義します。
- インテグレーション先が AWS のサービスのため、
--type AWS
とします。 - API Gateway から Lambda 関数へは POST でリクエストします (
--integration-http-method POST
)。 Lambda 関数は--uri
で指定します。
uri は "arn:aws:apigateway:REGION:lambda:path/2015-03-31/functions/LAMBDA_FUNCTION_ARN/invocations"
という形をしていて、 具体的には "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations"
というようになります。
$ aws apigateway put-integration \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --integration-http-method POST \ --type AWS \ --uri "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations" { "httpMethod": "POST", "type": "AWS", "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations", "cacheNamespace": "zzzzzz" }
put-method-response(API Gateway からクライアントへのレスポンス)
API Gateway からクライアントへのレスポンスを定義します。
レスポンスは JSON 形式のため --response-models '{"application/json": "Empty"}'
とします。
"Empty" はモデルの名前です。
モデルは API ごとに存在し CLI からは $ aws apigateway get-models --rest-api-id $REST_API_ID
で確認出来ます。
$ aws apigateway put-method-response \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --status-code 200 \ --response-models '{"application/json": "Empty"}' { "responseModels": { "application/json": "Empty" }, "statusCode": "200" }
put-integration-response(Lambda から API Gateway へのレスポンス)
Lambda から API Gateway へのレスポンスを定義します。
API Gateway ではテンプレートと言う概念があり、Lambda 関数のレスポンスに対してスキーマ変換出来ます。
http://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html
--response-templates
引数でこのテンプレートを指定します。
今回はスキーマ変換せず、 JSON をそのまま返すので --response-templates '{"application/json": null}'
としたいところなのですが、AWS CLI にバグが あるため、ワークアラウンドとして --response-templates '{"application/json": ""}'
と空文字を指定します。
BUG URL https://github.com/aws/aws-cli/issues/1610
$ aws apigateway put-integration-response \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --status-code 200 \ --response-templates '{"application/json": null}' Parameter validation failed: Invalid type for parameter responseTemplates.application/json, value: None, type: <type 'NoneType'>, valid types: <type 'basestring'> # workaround $ aws apigateway put-integration-response \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --status-code 200 \ --response-templates '{"application/json": ""}' { "statusCode": "200", "responseTemplates": { "application/json": null } }
Lambda 関数のパーミッション変更
最後に、API Gateway が Lambda 関数を invoke できるように Lambda 関数のパーミッションを変更します。
lambda add-permission API を使います。
--action
は"lambda:InvokeFunction"
--principal
はapigateway.amazonaws.com
--source-arn
は"arn:aws:execute-api:ap-northeast-1:ACOUNT_ID:API_ID/*/HTTP_METHOD/RESOURCE_NAME"
とします。
$ aws lambda add-permission --function-name GetHelloWorld \ --statement-id f3385cd3-c2f8-49e5-ba72-d2801e5f93d8 \ --action "lambda:InvokeFunction" \ --principal apigateway.amazonaws.com \ --source-arn "arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/GET/mydemoresource" { "Statement": "{\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/GET/mydemoresource\"}},\"Action\":[\"lambda:InvokeFunction\"],\"Resource\":\"arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Sid\":\"f3385cd3-c2f8-49e5-ba72-d2801e5f93d8\"}" } $ aws lambda get-policy --function-name GetHelloWorld | jq -r .Policy | jq . { "Version": "2012-10-17", "Statement": [ { "Condition": { "ArnLike": { "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/GET/mydemoresource" } }, "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld", "Effect": "Allow", "Principal": { "Service": "apigateway.amazonaws.com" }, "Sid": "f3385cd3-c2f8-49e5-ba72-d2801e5f93d8" } ], "Id": "default" }
GET メソッドのテスト実行
ここで定義した mydemoresource
リソースの GET メソッドを確認します。
まずは定義を確認確認します。
$ aws apigateway get-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method GET { "apiKeyRequired": false, "httpMethod": "GET", "methodIntegration": { "integrationResponses": { "200": { "responseTemplates": { "application/json": null }, "statusCode": "200" } }, "cacheKeyParameters": [], "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWorld/invocations", "httpMethod": "POST", "cacheNamespace": "zzzzzz", "type": "AWS" }, "requestParameters": {}, "methodResponses": { "200": { "responseModels": { "application/json": "Empty" }, "statusCode": "200" } }, "authorizationType": "NONE" }
GET メソッドを実際に呼び出しましょう。
$ aws apigateway test-invoke-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method GET --path-with-query-string '' { "status": 200, "body": "{\"Hello\":\"World\"}", "log": "Execution log for request test-request...[snip]...Successfully completed execution\n", "latency": 1111, "headers": { "Content-Type": "application/json" } }
status が 200 で body の内容から期待どおりのレスポンスがかえってきています。
6. POST メソッドを作成してテストする
GET メソッドと同じ要領でリソース mydemoresource
に POST すると Lambda 関数 GetHelloWithName
を呼び出すように POST メソッドを定義します。
- Method Request(クライアントから API Gateway へのリクエスト)
- put-integration(API Gateway から Lambda へのリクエスト)
- put-integration-response(Lambda から API Gateway へのレスポンス)
- put-method-response(API Gateway からクライアントへのレスポンス)
という4項目をそれぞれ AWS CLI から定義します。
Black Blet Tech シリーズ Amazon API Gateway の P.49 から先も合わせてご確認ください。
Method Request(クライアントから API Gateway へのリクエスト)
リソース /mydemoresource に対して POST メソッドを用意します。(--http-method POST) 認証なしでリクエストできるようにするため --authorization-type None とします。
$ aws apigateway put-method \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method POST \ --authorization-type NONE \ --no-api-key-required \ --request-parameters {} { "apiKeyRequired": false, "httpMethod": "POST", "authorizationType": "NONE", "requestParameters": {} }
put-integration(API Gateway から Lambda へのリクエスト)
GET の時と同じです。
$ aws apigateway put-integration \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method POST \ --integration-http-method POST \ --type AWS \ --uri "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName/invocations" { "httpMethod": "POST", "type": "AWS", "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName/invocations", "cacheNamespace": "zzzzzz" }
put-integration-response(Lambda から API Gateway へのレスポンス)
GET の時と同じです。
$ aws apigateway put-method-response \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method POST \ --status-code 200 \ --response-models '{"application/json": "Empty"}' { "responseModels": { "application/json": "Empty" }, "statusCode": "200" }
put-method-response(API Gateway からクライアントへのレスポンス)
GET の時と同じです。
$ aws apigateway put-integration-response \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method POST \ --status-code 200 \ --response-templates '{"application/json": ""}' { "statusCode": "200", "responseTemplates": { "application/json": null } }
Lambda 関数のパーミッション変更
最後に、API Gateway が Lambda 関数を invoke できるように Lambda 関数のパーミッションを変更します。
GET の時とは
--function-name
(Lambda 関数名)--source-arn
(GET から POST への変更)
が変わっています。
$ aws lambda add-permission --function-name GetHelloWithName \ --statement-id 72e0a706-02e8-479f-affb-0a2dcc5d4a29 \ --action "lambda:InvokeFunction" \ --principal apigateway.amazonaws.com \ --source-arn "arn:aws:execute-api:ap-northeast-1:1234567890:asdfqwer/*/POST/mydemoresource"
POST メソッドのテスト実行
ここで定義した mydemoresource リソースの POST メソッドを確認します。
$ aws apigateway get-method --rest-api-id $REST_API_ID --resource-id $RESOURCE_ID --http-method POST { "apiKeyRequired": false, "httpMethod": "POST", "methodIntegration": { "integrationResponses": { "200": { "responseTemplates": { "application/json": null }, "statusCode": "200" } }, "cacheKeyParameters": [], "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:1234567890:function:GetHelloWithName/invocations", "httpMethod": "POST", "cacheNamespace": "zzzzzz", "type": "AWS" }, "requestParameters": {}, "methodResponses": { "200": { "responseModels": { "application/json": "Empty" }, "statusCode": "200" } }, "authorizationType": "NONE" }
POST メソッドを実際に呼び出しましょう。
POST リクエスト時のボディーは --body
で渡します。
$ aws apigateway test-invoke-method \ --rest-api-id $REST_API_ID \ --resource-id $RESOURCE_ID \ --http-method POST \ --path-with-query-string '' \ --body '{"name":"John"}' { "status": 200, "body": "{\"Hello\":\"John\"}", "log": "Execution log for request test-request...Successfully completed execution\n", "latency": 69, "headers": { "Content-Type": "application/json" } }
レスポンスの body が {"Hello":"John"} となっており、POST リクエスト内容が反映されています。
7. API をデプロイする
定義したAPIを "test" ステージにデプロイします。
https://REST_API_ID.execute-api.REGION.amazonaws.com/STAGE_NAME/RESOURCE_NAME
の形式でアクセスできるようになります。
$ aws apigateway create-deployment \ --rest-api-id $REST_API_ID \ --stage-name test \ --stage-description "This is a test" \ --description "Calling Lambda functions walkthroug" { "description": "Calling Lambda functions walkthroug", "id": "aaaaaa", "createdDate": 1446288905 } $ aws apigateway get-deployments --rest-api-id $REST_API_ID { "items": [ { "createdDate": 1446288905, "id": "aaaaaa", "description": "Calling Lambda functions walkthroug" } ] } $ aws apigateway get-stages --rest-api-id $REST_API_ID { "item": [ { "description": "This is a test", "stageName": "test", "cacheClusterEnabled": false, "cacheClusterStatus": "NOT_AVAILABLE", "deploymentId": "aaaaaa", "lastUpdatedDate": 1446288905, "createdDate": 1446288905, "methodSettings": {} } ] }
8. API をテストする
curl から GET/POST それぞれでリソース mydemoresource
にアクセスします。
GET メソッド
$ curl https://$REST_API_ID.execute-api.ap-northeast-1.amazonaws.com/test/mydemoresource {"Hello":"World"}
POST メソッド
$ curl -H "Content-Type: application/json" -X POST -d "{\"name\": \"John\"}" https://$REST_API_ID.execute-api.ap-northeast-1.amazonaws.com/test/mydemoresource {"Hello":"John"}
期待通りですね。
9. クリーンアップ
最後に API を削除します。
$ aws apigateway delete-rest-api --rest-api-id $REST_API_ID $ aws apigateway get-rest-apis { "items": [] }
Hypertext Application Language(HAL) について
API Gateway の REST API Reference にあるように、この API は JSON HAL をしゃべります。
The Amazon API Gateway web service is a resource-based API that uses Hypertext Application Language (HAL). http://docs.aws.amazon.com/apigateway/api-reference/
試しにcreate-rest-api
API を --debug オプション付きで実行してみましょう。
$ aws apigateway create-rest-api --name dummy --debug ... 2015-10-31 19:59:18,003 - MainThread - botocore.vendored.requests.packages.urllib3.connectionpool - DEBUG - "POST /restapis HTTP/1.1" 201 120 2015-10-31 19:59:18,004 - MainThread - botocore.parsers - DEBUG - Response headers: {'x-amzn-requestid': '6c872d07-7fbe-11e5-bcdc-93557855b8e7', 'date': 'Sat, 31 Oct 2015 10:59:14 GMT', 'content-length': '120', 'content-type': 'application/json'} 2015-10-31 19:59:18,004 - MainThread - botocore.parsers - DEBUG - Response body: {"createdDate":1446289154,"description":"This is my API for demonstration purposes","id":"fdp48380xl","name":"DemoAPI"} ...
確かに
- HTTP ステータス
201:Created
'content-type': 'application/json'
- レスポンスボディー
{"createdDate":1446289154,"description":"This is my API for demonstration purposes","id":"fdp48380xl","name":"DemoAPI"}
という JSON が返ってきていますね。
これまでの XML との決裂をわかった上で API Gateway だけ独自路線で HAL をねじ込んできた API Gateway のプロダクトマネージャーやアーキテクトは相当なやり手と思われ、周りを巻き込んだリリースまでの道のりについてどこかで語って欲しいですね。
まとめ
生 REST API は辛い。