この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
おはようございます、もきゅりんです。
あけましておめでとうございます。
今回は、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つをチェックします。
- クロスオリジン HTTP リクエストかどうか
- シンプルなリクエストかシンプルではないリクエストかどうか
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
と書かれた素朴なページがありますので、クリックします。
エラー表示されました。
表を確認します。
そうでした、クロスドメインなので、レスポンス対応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."})
}
完了したらまたクリックします。
OKOK〜成功フォ〜。
POSTしてみる
じゃ次はPOSTしてみましょう。
handler.py
とindex.html
、serverless.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
します。
エラーです。
Oh, Why ?
index.html
を確認すると contentType: "application/json"
と記載されています。
Content-Typeは text/plain
、multipart/form-data
、application/x-www-form-urlencoded
じゃないぜ。
ってことは、シンプルなリクエストじゃないから、Access-Control-Allow-Origin
, Access-Control-Allow-Headers
ヘッダーが必要だぜ。
下記のようにhandler.py
をAccess-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'])
}
そしたらデプロイしてクリックします。
Oh, またダメだよ!なぜだよ!
そうでした、ブラウザがシンプルではない HTTP リクエストを受信した場合は、ブラウザに実際のリクエストを送信する前に、サーバーに preflightリクエスト を送るのでした。。
CORS をサポートするために、REST API リソースは、OPTIONSメソッドで preflightリクエスト に応答できる必要がありました。
つまり、CORS を有効化ですね。
コンソールだとここですが、
serverless.yml
に cors: true
を追記してデプロイします。
....
functions:
lambda_handler:
handler: handler.lambda_handler
events:
- http:
path: /
method: POST
cors: true
....
有効化されますね。
試します。
OKOK〜成功フォ〜ゥ。
greedyパス変数とANY使ってみる
OK, そしたら今度はgreedyパス変数とANY使ってみるぜ。
serverless.yml
と index.html
を下記のように更新してデプロイするぜ。
....
functions:
lambda_handler:
handler: handler.lambda_handler
events:
- http:
path: /{proxy+}
method: ANY
....
....
var URL = 'APIGATEWAY_ENDPOINT/api';
....
コンソールだとこんな感じです。
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対応をやってみながらまとめてみました。
途中から何だか楽しくなってきて、語尾がおかしくなってしまいました。
大変失礼致しました。
以上です。
どなたかのお役に立てば幸いです。
参考:
- API Gateway REST API リソースの CORS を有効にする
- CORS(Cross-Origin Resource Sharing)について整理してみた
- Serverless Framework / API Gateway
- 初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~
- ServerlessFrameworkでS3の静的サイトのホスティングをする