こんにちは、広野です。
サーバーレスアプリケーションをつくるときに、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 関数を連携するときの、あくまでも構成例のひとつではありますが、みなさまのお役に立てれば幸いです。