こんにちは、広野です。
サーバーレスアプリケーションをつくるときに、Amazon API Gateway と AWS Lambda の組み合わせは王道ですが、連携させるときにエラーが連発することが日常茶飯事な方は少なからずいらっしゃるのではないでしょうか?
私自身がそうだったのですが、あるときから「動く構成を AWS CloudFormation テンプレートで標準化」してしまって、そのコピペでプロビジョニングすることにより、エラー地獄から解放されました。
今回は、一例ではありますが Amazon API Gateway と AWS Lambda の連携時によくハマる原因と、正常に動く Amazon API Gateway の構成パターンを AWS CloudFormation テンプレート付きで紹介したいと思います。
やりたいこと
- Amazon API Gateway と AWS Lambda を連携させるときにハマりたくない。
- Amazon API Gateway の設定は、以下で決め打ちとする。(いろんなパターンはつくらない)
- メソッドは POST
- 統合タイプは Lambda 関数
- Lambda プロキシ統合の使用 を有効にする(REST API の場合)
- アプリからは、Amazon API Gateway に以下のデータを送る。(例)
{ "p1": "data1", "p2": "data2", "p3": "data3" }
Amazon API Gateway ではあれこれデータ変換などせず、そのまま AWS Lambda 関数にパススルーするイメージです。
ハマる原因
多くの場合、Amazon API Gateway と AWS Lambda 連携時にハマり、時間を浪費しやすいのは以下のパターンだと思っています。もちろん他にもあると思いますが、他はまだ原因が判明しやすいです。
- AWS Lambda 関数でデータを受け取る変数の書き方
- CORSの設定
- Amazon API Gateway のデプロイ忘れ
ハマらないようにするために
AWS Lambda 関数でデータを受け取る変数の書き方
Amazon API Gateway から AWS Lambda 関数にデータが渡されるとき、AWS Lambda 関数側では event という変数で受け取るとします。
そのとき、アプリが送ったデータは event[‘body’] の中に格納されていますが、データ型が文字列になっています。
したがって、event[‘body’] に格納されたデータ項目を取得しようと思ったら、event[‘body’] のデータ型をまずは JSON オブジェクトに変更して取得しなければなりません。ストレートに event[‘body’][‘p1’] と書くとエラーになります。
Python の場合
import json def lambda_handler(event, context): params = json.loads(event['body']) p1 = params['p1'] #中身は data1 p2 = params['p2'] #中身は data2 p3 = params['p3'] #中身は data3
Node.js の場合
exports.handler = async(event) => { const params = JSON.parse(event.body); const p1 = params.p1 #中身は data1 const p2 = params.p2 #中身は data2 const p3 = params.p3 #中身は data3 };
CORS の設定
CORS のエラーは最も頻発し、かつ原因究明がしづらいです。ブラウザの開発者ツールにエラーが表示されても、それだけでは原因が掴めないのが厄介で、とにかく適切に関連リソースの CORS 設定をすることが求められます。
Amazon API Gateway 側の設定
アプリケーションから axios 等の API 呼出モジュールを使用して Amazon API Gateway にリクエストをするとき、その API 呼出には「プリフライトチェック」と呼ばれる通信事前チェックが行われます。
プリフライトチェックでは、OPTIONS メソッドが使用されるので、Amazon API Gateway 側のメソッドに POST だけでなく OPTIONS を追加しておく必要があります。
また、実は本記事の構成ですと不要とは思いますが、おまじない的に CORS の設定もしておきましょう。
AWS Lambda 関数側の設定
本記事の構成では、CORSの設定は AWS Lambda 関数側で持っておく必要があります。Amazon API Gateway へのリクエストを許可するアプリケーションのドメイン名等を登録しておきます。
サンプルですが、以下のように AWS Lambda 関数から Amazon API Gateway にレスポンス (return) するように書いておくと、動きます。Python, Node.js での違いは文法だけです。
Python の場合
return { "isBase64Encoded": False, "statusCode": 200, "headers": { "Access-Control-Allow-Headers": "Content-Type,Authorization", "Access-Control-Allow-Origin": "https://xxx.example.com", #アクセスを許可するドメイン "Access-Control-Allow-Methods": "OPTIONS,POST" }, "body": json.dumps(res) #resがJSONオブジェクトのときは文字列に変換して戻すのもなにげに重要 }
Node.js の場合
return { "isBase64Encoded": false, "statusCode": 200, "headers": { "Access-Control-Allow-Headers": "Content-Type,Authorization", "Access-Control-Allow-Origin": "https://xxx.example.com", //アクセスを許可するドメイン "Access-Control-Allow-Methods": "OPTIONS,POST" }, "body": JSON.stringify(res) //resがJSONオブジェクトのときは文字列に変換して戻すのもなにげに重要 };
Amazon API Gateway のデプロイ忘れ
Amazon API Gateway は設定を変更した後に必ずデプロイをする必要があります。そうしないと変更が反映されません。
HTTP API であれば自動デプロイの設定があり、設定変更後に自動でデプロイしてくれます。特別な事情がなければ、安全のために自動デプロイを活用した方が良いと思います。
REST API には自動デプロイの設定がありません。無理やり自動デプロイさせる仕組みも作り込みできなくはないのですが大変です。ここは今後の改善が待たれるところです。
もう1つ、経験則なのですが注意すべきことがあります。
統合タイプを Lambda 関数 にしている場合、Lambda 関数内の CORS 関連設定を変更した後は必ず Amazon API Gateway の再デプロイをしましょう。と言うのは、Amazon API Gateway はデプロイ時に、関連付けられた Lambda 関数から CORS 設定を読み取っている挙動をしていたからです。あくまで経験則なのですが、これが原因でハマったことがありましたので。
サンプル AWS CloudFormation テンプレート
私がコピペして使うテンプレートです。実際には Amazon Cognito オーソライザーもセットでプロビジョニングするケースの方が圧倒的に多いのですが、本記事ではそれを除いたシンプルなテンプレートにしています。AWS Lambda 関数のテンプレートは省略します。
REST API 用
AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template that creates API Gateways and relevant IAM roles. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: The system name. Default: example MaxLength: 10 MinLength: 1 # ------------------------------------------------------------# # API Gateway Invocation Role (IAM) # ------------------------------------------------------------# ApiGatewayLambdaInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ApiGatwayLambdaInvocationRole-${SystemName} Description: This role allows API Gateways to invoke Lambda functions. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaRole - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/AmazonKinesisFirehoseFullAccess - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess # ------------------------------------------------------------# # API Gateway # ------------------------------------------------------------# RestApiExampledata: Type: AWS::ApiGateway::RestApi Properties: Name: !Sub RestApi-Exampledata-${SystemName} Description: !Sub REST API Gateway to access example data for ${SystemName} EndpointConfiguration: Types: - REGIONAL Tags: - Key: Cost Value: !Ref SystemName RestApiDeploymentExampledata: Type: AWS::ApiGateway::Deployment Properties: RestApiId: !Ref RestApiExampledata DependsOn: - RestApiMethodGetExampledataPost - RestApiMethodGetExampledataOptions RestApiStageExampledata: Type: AWS::ApiGateway::Stage Properties: StageName: prod Description: production stage RestApiId: !Ref RestApiExampledata DeploymentId: !Ref RestApiDeploymentExampledata TracingEnabled: true AccessLogSetting: DestinationArn: #アクセスログ取得用 Amazon Kinesis Data Firehose ストリームのARNを記載 Format: '{"requestId":"$context.requestId","status":"$context.status","resourcePath":"$context.resourcePath","requestTime":"$context.requestTime","sourceIp":"$context.identity.sourceIp","userAgent":"$context.identity.userAgent","apigatewayError":"$context.error.message","authorizerError":"$context.authorizer.error","integrationError":"$context.integration.error"}' Tags: - Key: Cost Value: !Ref SystemName RestApiResourceGetExampledata: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref RestApiExampledata ParentId: !GetAtt RestApiExampledata.RootResourceId PathPart: GetExampledata RestApiMethodGetExampledataPost: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApiExampledata ResourceId: !Ref RestApiResourceGetExampledata HttpMethod: POST AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Credentials: !GetAtt ApiGatewayLambdaInvocationRole.Arn Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:ここにLambda関数名を記載/invocations PassthroughBehavior: WHEN_NO_MATCH MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Origin: true RestApiMethodGetExampledataOptions: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApiExampledata ResourceId: !Ref RestApiResourceGetExampledata HttpMethod: OPTIONS AuthorizationType: NONE Integration: Type: MOCK Credentials: !GetAtt ApiGatewayLambdaInvocationRole.Arn IntegrationResponses: - ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'" method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'https://xxx.example.com'" ResponseTemplates: application/json: '' StatusCode: 200 PassthroughBehavior: WHEN_NO_MATCH RequestTemplates: application/json: '{"statusCode": 200}' MethodResponses: - ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true StatusCode: 200
HTTP API 用
AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template that creates API Gateways and relevant IAM roles. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: The system name. Default: example MaxLength: 10 MinLength: 1 # ------------------------------------------------------------# # API Gateway Invocation Role (IAM) # ------------------------------------------------------------# ApiGatewayLambdaInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ApiGatwayLambdaInvocationRole-${SystemName} Description: This role allows API Gateways to invoke Lambda functions. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaRole - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/AmazonKinesisFirehoseFullAccess - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess # ------------------------------------------------------------# # API Gateway # ------------------------------------------------------------# HttpApiExampledata: Type: AWS::ApiGatewayV2::Api Properties: Name: !Sub HttpApi-Exampledata-${SystemName} Description: !Sub HTTP API Gateway to access example data for ${SystemName} ProtocolType: HTTP CorsConfiguration: AllowCredentials: false AllowHeaders: - "*" AllowMethods: - POST - OPTIONS AllowOrigins: - https://xxx.example.com ExposeHeaders: - "*" MaxAge: 600 DisableExecuteApiEndpoint: false Tags: Cost: !Ref SystemName HttpApiIntegrationExampledata: Type: AWS::ApiGatewayV2::Integration Properties: ApiId: !Ref HttpApiExampledata IntegrationMethod: POST IntegrationType: AWS_PROXY IntegrationUri: #Lambda関数のARNを記載 CredentialsArn: !GetAtt ApiGatewayLambdaInvocationRole.Arn PayloadFormatVersion: 2.0 TimeoutInMillis: 5000 HttpApiRouteExampledata: Type: AWS::ApiGatewayV2::Route Properties: ApiId: !Ref HttpApiExampledata RouteKey: POST /Exampledata Target: !Sub integrations/${HttpApiIntegrationExampledata} HttpApiStageExampledata: Type: AWS::ApiGatewayV2::Stage Properties: ApiId: !Ref HttpApiExampledata AutoDeploy: true StageName: $default
HTTP API は OPTIONS メソッドを明示的に作らなくても、自動的に作成されます。HTTP API の方が設定項目が少なくシンプルです。
まとめ
いかがでしたでしょうか?
Amazon API Gateway と AWS Lambda 関数を連携するときの、あくまでも構成例のひとつではありますが、みなさまのお役に立てれば幸いです。