Amazon API Gateway と AWS Lambda の連携でもうハマりたくない [AWS CloudFormation テンプレート付き]

こんにちは、広野です。

サーバーレスアプリケーションをつくるときに、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 連携時にハマり、時間を浪費しやすいのは以下のパターンだと思っています。もちろん他にもあると思いますが、他はまだ原因が判明しやすいです。

  1. AWS Lambda 関数でデータを受け取る変数の書き方
  2. CORSの設定
  3. 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 関数を連携するときの、あくまでも構成例のひとつではありますが、みなさまのお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもAWSすげー!って気持ちの方が強いです。
取得資格:AWS 認定は12資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2023 Japan AWS Top Engineer 受賞
2022 - 2023 Japan AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました