API Gateway の Lambda プロキシ統合のCORS対応をまとめてみる

2020.01.31

はじめに

おはようございます、もきゅりんです。

あけましておめでとうございます。

今回は、Lambda プロキシ統合のCORS対応をまとめてみました。

まず、この記事の内容に興味はあるのだが、Lambda プロキシ統合とかカスタム統合(非プロキシ統合)とか何?という方はこちらご参照下さい。

[初心者向け] Lambda 非プロキシ統合で API Gateway API をビルドする をプロキシ統合にして比較してみる

1 CORSについておさらい

CORSって何ぞや、または何となく知ってるけども、フワフワしておるという方は下記記事が非常におすすめです。

CORS(Cross-Origin Resource Sharing)について整理してみた

とにかく、API Gateway REST API(Lambda または HTTP プロキシ統合) の CORSの対応について知りたいという方は下表をご確認下さい。

クロスオリジンか シンプルリクエストである やること 内容
レスポンス対応1 Access-Control-Allow-Origin ヘッダー
CORS有効化 *1 + レスポンス対応2 Access-Control-Allow-Origin , Access-Control-Allow-Headers ヘッダー
なし -

内容としては、以下 2つをチェックします。

  1. クロスオリジン HTTP リクエストかどうか
  2. シンプルなリクエストかシンプルではないリクエストかどうか

API Gateway REST API リソースの CORS を有効にする の再掲です。

1. クロスオリジン HTTP リクエストかどうか

以下クロスオリジンHTTPリクエストに当てはまるケースです。

  • 別ドメイン (例: example.com から amazondomains.com へ)
  • 別サブドメイン (例: example.com から petstore.example.com へ)
  • 別ポート (例: example.com から example.com:10777 へ)
  • 別プロトコル (例: https://example.com から http://example.com へ)

2. シンプルなリクエストかシンプルではないリクエストかどうか

以下条件がすべて当てはまる場合、シンプルなリクエストです。当てはまらない場合は、シンプルではないリクエストです。

  • GET、HEAD、および POST のいずれかのメソッドリクエスト
  • POST メソッドリクエストの場合、Origin ヘッダーを含まれている
  • Content-Typeは text/plain、multipart/form-data、または application/x-www-form-urlencoded のどれか
  • リクエストにカスタムヘッダーが含まれていない
  • シンプルなリクエストに関する Mozilla CORS のドキュメント に一覧表示されている追加要件

整理できましたでしょうか。

Lambda または HTTP プロキシ統合以外の対応については、別途ドキュメント をご確認下さい。

実際に試してみる

まとめた内容をS3とAPI Gatewayを使って実際にやってみます。

構築は簡単な構成なので、Serverless Frameworkを使います。

Serverless Frameworkで何をやっているかを確認したい方はこちら参考にして下さい。

Serverless Framework / Quick Start

[初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~

環境は設定が楽なので、上記と同様 Cloud9 で進めていきます。

GETしてみる

適当なスペックのインスタンスを立ち上げて、Serverless Frameworkをインストールしたら、サービスを作成します。

$ serverless create --template aws-python3 --path demo-cors-api
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/home/ec2-user/environment/demo-cors-api"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.61.3
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"

handler.py は、そのままでも問題ないのですが、いちおう書き換えておきます。

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps({"result": "GET Method Success."})
    }

serverless.ymlを下記のように書き換えます。

リージョンをap-northeast-1に設定します。

API Gatewayのエンドポイントは、デフォルトでエッジ最適化で作成してしまうようなので、リージョンに設定します。

service: demo-cors-api

provider:
  name: aws
  runtime: python3.8
  endpointType: REGIONAL
  region: ap-northeast-1

functions:
  lambda_handler:
    handler: handler.lambda_handler
    events:
      - http:
          path: /
          method: GET

そしたらデプロイします。

$ cd demo-cors
$ sls deploy -v

出来上がったらエンドポイントのURLが表示されるので控えておきます。

...
Stack Outputs
LambdaUnderscorehandlerLambdaFunctionQualifiedArn: arn:aws:lambda:ap-northeast-1:xxxxxxx:function:demo-cors-dev-lambda_handler:1
ServiceEndpoint: https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
ServerlessDeploymentBucketName: demo-cors-dev-serverlessdeploymentbucket-xxxxxxxx

次に、今回はServerlessFramework経由で静的ファイルをS3にアップロードしたいので、プラグインをインストールします。

$ npm install --save serverless-s3-sync

staticというディレクトリを作成して、その中にindex.htmlを作成します。

$ mkdir static && touch static/index.html

index.html を更新します。

var URLには控えたエンドポイントのURLを代入します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>DemoForm</title>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script>
      $(function() {
        var URL = 'APIGATEWAY_ENDPOINT';
        $('#submit').click(function() {
          $.ajax({
            method: 'GET',
            url: URL
          })
            .done(function(msg) {
              console.log(msg);
              alert('success');
            })
            .fail(function(msg) {
              console.log(msg);
              alert('error');
            })
            .always(function() {
              alert('complete');
            });
        });
      });
    </script>
  </head>
  <body>
    <input type="button" id="submit" value="Go" />
  </body>
</html>

serverless.ymlに下記追記します。

...
...
custom:
  webSiteName: s3-demo-site
  s3Sync:
    - bucketName: ${self:custom.webSiteName}
      localDir: static
resources:
  Resources:
    StaticSite:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.webSiteName}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
    StaticSiteS3BucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket:
          Ref: StaticSite
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action:
              - s3:GetObject
              Resource:
                Fn::Join: ["", ["arn:aws:s3:::",{"Ref": "StaticSite"},"/*"]]

plugins:
  - serverless-s3-sync

そしたら再度デプロイします。

$ sls deploy -v

出来たらS3のStatic website hostingのエンドポイントを表示します。

GETと書かれた素朴なページがありますので、クリックします。

api21

error1

エラー表示されました。

表を確認します。

そうでした、クロスドメインなので、レスポンス対応1をしないといけないのでした。

レスポンス対応1はAccess-Control-Allow-Origin ヘッダーが必要です。

ということで、handler.py を修正してデプロイします。

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Origin": "*"
        },
        'body': json.dumps({"result": "GET Method Success."})
    }

完了したらまたクリックします。

success

OKOK〜成功フォ〜。

POSTしてみる

じゃ次はPOSTしてみましょう。

handler.pyindex.htmlserverless.yml をそれぞれ下記のように更新してデプロイします。

# handler.py
import json


def lambda_handler(event, context):
    print(event['httpMethod'])
    return {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Origin": "*"
        },
        'body': json.dumps(event['body'])
    }
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>DemoForm</title>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script>
      $(function() {
        var URL = 'APIGATEWAY_ENDPOINT';
        $('#submit').click(function() {
          var JSONdata = {
            name: $('#send-name').val(),
            subject: $('#send-subject').val(),
            checked: $('input:radio:checked').val()
          };
          $.ajax({
            method: 'POST',
            url: URL,
            dataType: 'json',
            contentType: "application/json",
            data: JSON.stringify(JSONdata)
          })
            .done(function(msg) {
              console.log(msg);
              alert('success');
            })
            .fail(function(msg) {
              console.log(msg);
              alert('error');
            })
            .always(function() {
              alert('complete');
            });
        });
      });
    </script>
  </head>
  <body>
    <p>
      Name:<br />
      <input type="text" id="send-name" name="name" />
    </p>
    <p>
      Subject:<br />
      <input type="text" id="send-subject" name="subject" />
    </p>

    <p>
      <input type="radio" name="maru" value="TRUE" checked="checked" />O
      <input type="radio" name="maru" value="FALSE" />X
    </p>
    <input type="button" id="submit" value="Go" />
  </body>
</html>
# serverless.yml
....
functions:
  lambda_handler:
    handler: handler.lambda_handler
    events:
      - http:
          path: /
          method: POST
....
              Action:
              - s3:GetObject
              - s3:PutObject

今度は入力欄があります。

post-form

適当に入力してPOSTします。

error2

エラーです。

Oh, Why ?

index.html を確認すると contentType: "application/json" と記載されています。

Content-Typeは text/plainmultipart/form-dataapplication/x-www-form-urlencoded じゃないぜ。

ってことは、シンプルなリクエストじゃないから、Access-Control-Allow-Origin , Access-Control-Allow-Headers ヘッダーが必要だぜ。

下記のようにhandler.pyAccess-Control-Allow-Headers ヘッダーを追記して更新します。 *2

import json


def lambda_handler(event, context):
    print(event['httpMethod'])
    return {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "Content-Type",
        },
        'body': json.dumps(event['body'])
    }

そしたらデプロイしてクリックします。

error3

Oh, またダメだよ!なぜだよ!

そうでした、ブラウザがシンプルではない HTTP リクエストを受信した場合は、ブラウザに実際のリクエストを送信する前に、サーバーに preflightリクエスト を送るのでした。。

CORS をサポートするために、REST API リソースは、OPTIONSメソッドで preflightリクエスト に応答できる必要がありました。

つまり、CORS を有効化ですね。

コンソールだとここですが、

enable-cors

serverless.ymlcors: true を追記してデプロイします。

....
functions:
  lambda_handler:
    handler: handler.lambda_handler
    events:
      - http:
          path: /
          method: POST
          cors: true
....

有効化されますね。

cors-enbaled

試します。

success

OKOK〜成功フォ〜ゥ。

greedyパス変数とANY使ってみる

OK, そしたら今度はgreedyパス変数とANY使ってみるぜ。

serverless.ymlindex.html を下記のように更新してデプロイするぜ。

....
functions:
  lambda_handler:
    handler: handler.lambda_handler
    events:
      - http:
          path: /{proxy+}
          method: ANY
....
....
var URL = 'APIGATEWAY_ENDPOINT/api';
....

コンソールだとこんな感じです。

greedy-any

POSTしてみます。

Oh, CORS有効化してないのにできたぜ

CORS をサポートするために、REST API リソースは、フェッチ標準で必要な次のレスポンスヘッダーを使用して、少なくとも OPTIONS プリフライトリクエストに応答できる OPTIONS メソッドを実装する必要があります。

ANYメソッドは、元々OPTIONSにも対応できちゃうので、CORSの有効化をする必要がないのですね。

これは楽 & 便利だぜ。

説明しよう。

プロキシ統合とANYメソッドとgreedyパス変数について

プロキシリソースとのプロキシ統合を設定する

よく一緒に使用されますが、greedy パス変数 `{proxy+} `、ANY メソッド、(HTTP/Lambda)プロキシ統合タイプはそれぞれ独立した機能です。

  • プロキシ統合とは?

Lambda プロキシ統合では、クライアントが API リクエストを送信すると、API Gateway は、統合された Lambda 関数に raw リクエストをそのまま渡します。

  • ANYメソッドとは?

汎用的な HTTP メソッドです。DELETE、GET、HEAD、OPTIONS、PATCH、POST および PUT のサポートされるすべての HTTP メソッドに対応します。

  • greedyパス変数とは?

    {proxy+} の代わりに特定の URL パスを指定し、要求されるヘッダー、クエリ文字列パラメータ、または適切なペイロードを含めます

これで整理ができて、対応方法も確認できました。

そしたら、環境は消しましょう。

sls remove -v

最後に

Lambda プロキシ統合のCORS対応をやってみながらまとめてみました。

途中から何だか楽しくなってきて、語尾がおかしくなってしまいました。

大変失礼致しました。

以上です。

どなたかのお役に立てば幸いです。

参考:

脚注

  1. 必要ない場合もあります。後述。
  2. 実環境においては、レスポンスヘッダはより詳細な設定になると思いますので、仕様を確認の上、設定する必要があるかと思います。