🎯 概要
SPA(シングルページアプリケーション)や静的サイトから AWS Lambda + API Gateway 経由でデータを取得する構成は効率的なアーキテクチャとして広く採用されています。しかし、APIに対してCORS(Cross-Origin Resource Sharing)設定が不十分だと「CORSエラー」で動作しないという問題がよく発生します。
本記事では、API Gateway + Lambda の構成で「CORS制限」を正しく設定する手順を詳細に解説し、一般的なトラブルシューティングと解決策も提供します。
📦 想定アーキテクチャ
- フロントエンド: CloudFront + S3 (静的ホスティング)
- API: API Gateway + Lambda (Node.js/Python)
- 目的: CloudFront から API Gateway を呼び出すときに
CORSエラーを出さず、他ドメインからのアクセスは制限する
🔍 CORS の基本原理
CORS とは何か
CORS は Cross-Origin Resource Sharing の略で、異なるオリジン(ドメイン、プロトコル、ポート)間でのリソース共有を制御するセキュリティメカニズムです。
ブラウザは同一オリジンポリシー(Same-Origin Policy)を実装しており、デフォルトでは異なるオリジンへのリクエストを制限します。CORS はこの制限を適切に緩和する仕組みを提供します。
CORS の流れ
- ブラウザが異なるオリジンへのリクエストを検出
- 実際のリクエスト前に プリフライトリクエスト (
OPTIONS
メソッド) を送信 - サーバーが適切な CORS ヘッダーで応答
- ブラウザが応答を評価し、許可されていれば実際のリクエストを実行
✅ 対策① — API Gateway 側のCORS設定
SAM テンプレートでの設定 (template.yaml
)
Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With'"
AllowOrigin: "'https://d3rcrq5ytlkzie.cloudfront.net'"
MaxAge: "'600'"
CloudFormation での設定 (template.yaml
)
Resources:
ApiGatewayRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: MyApi
# その他のプロパティ
ApiGatewayResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApiGatewayRestApi
ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
PathPart: "items"
ApiGatewayOptionsMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApiGatewayRestApi
ResourceId: !Ref ApiGatewayResource
HttpMethod: OPTIONS
AuthorizationType: NONE
Integration:
Type: MOCK
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With'"
method.response.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'https://d3rcrq5ytlkzie.cloudfront.net'"
method.response.header.Access-Control-Max-Age: "'600'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: WHEN_NO_MATCH
RequestTemplates:
application/json: '{"statusCode": 200}'
MethodResponses:
- StatusCode: 200
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
method.response.header.Access-Control-Max-Age: true
AWS コンソールでの設定
- API Gateway コンソールを開く
- API を選択 → リソースを選択
- アクション → CORS の有効化
- 以下の設定を入力:
- Access-Control-Allow-Origin:
https://d3rcrq5ytlkzie.cloudfront.net
- Access-Control-Allow-Headers:
Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With
- Access-Control-Allow-Methods:
GET,POST,PUT,DELETE,OPTIONS
- Access-Control-Max-Age:
600
- Access-Control-Allow-Origin:
- CORS の有効化およびメソッドの置換をクリック
⚠️ 重要なポイント
- シングルクオート(
'
)で値を囲む必要があります(YAML パースの問題を回避するため) AllowOrigin
には明示的に許可する CloudFront ドメインを指定しましょう- デプロイ後に変更を有効にするには API のステージを再デプロイする必要があります
✅ 対策② — Lambda 関数側で CORS ヘッダーを返す
Node.js での実装例
const headers = {
'Access-Control-Allow-Origin': 'https://d3rcrq5ytlkzie.cloudfront.net',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Content-Type': 'application/json',
};
exports.handler = async (event) => {
console.log('Event:', JSON.stringify(event));
// OPTIONS メソッド (プリフライトリクエスト) の処理
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// 通常の処理ロジック
const responseBody = {
message: 'Success!',
timestamp: new Date().toISOString()
};
return {
statusCode: 200,
headers, // すべてのレスポンスに CORS ヘッダーを含める
body: JSON.stringify(responseBody),
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
headers, // エラーレスポンスにも CORS ヘッダーを含める
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
Python での実装例
import json
import logging
from datetime import datetime
logger = logging.getLogger()
logger.setLevel(logging.INFO)
headers = {
'Access-Control-Allow-Origin': 'https://d3rcrq5ytlkzie.cloudfront.net',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Content-Type': 'application/json'
}
def lambda_handler(event, context):
logger.info(f"Event: {json.dumps(event)}")
# OPTIONS メソッド (プリフライトリクエスト) の処理
if event.get('httpMethod') == 'OPTIONS':
return {
'statusCode': 200,
'headers': headers,
'body': ''
}
try:
# 通常の処理ロジック
response_body = {
'message': 'Success!',
'timestamp': datetime.now().isoformat()
}
return {
'statusCode': 200,
'headers': headers, # すべてのレスポンスに CORS ヘッダーを含める
'body': json.dumps(response_body)
}
except Exception as e:
logger.error(f"Error: {str(e)}")
return {
'statusCode': 500,
'headers': headers, # エラーレスポンスにも CORS ヘッダーを含める
'body': json.dumps({'error': 'Internal server error'})
}
⚠️ 重要なポイント
- すべてのレスポンス(成功、エラーを問わず)に CORS ヘッダーを含める必要があります
OPTIONS
リクエストに対しては空のbody
を返します- API Gateway のプロキシ統合では、Lambda がヘッダーを含む完全なレスポンスオブジェクトを返す必要があります
✅ 対策③ — CloudFront 側の設定(ヘッダー転送)
CloudFront から API Gateway にリクエストを転送する場合、Origin
ヘッダーが API Gateway に届いていないと CORS 対応できません。これを確実にするための設定が必要です。
CloudFront で「Origin ヘッダーを転送」する手順
- CloudFront コンソールにアクセス
- ディストリビューションを選択 → 「ビヘイビア」タブを開く
- API Gateway 向けのビヘイビアを選択または作成
- 以下のいずれかの方法で設定:
方法1: キャッシュポリシーを使用(推奨)
- 「キャッシュポリシー」を「CachingDisabled」または「CORS-S3Origin」に設定
- または、カスタムキャッシュポリシーを作成し、「Origin」ヘッダーをキャッシュキーに含める
方法2: レガシーキャッシュ設定の場合
- 「レガシーキャッシュ設定」を選択
- 「オリジンへの転送ヘッダー」で「すべてのビューワーヘッダー」を選択 または
- 「ホワイトリストされたヘッダー」を選択し、「Origin」を追加
方法3: オリジンリクエストポリシーを使用
- 「オリジンリクエストポリシー」で「CORS-CustomOrigin」または「AllViewer」を選択
- または、カスタムオリジンリクエストポリシーを作成し、「Origin」ヘッダーを含める
⚠️ 重要なポイント
- CloudFront がヘッダーを転送しない場合、API Gateway は要求元のオリジンを認識できません
- キャッシュを有効にする場合は、ヘッダーに基づいてキャッシュキーを作成する設定が必要です
- 変更後は CloudFront ディストリビューションの再デプロイが必要です(15〜30分かかる場合があります)
✅ 対策④ — セキュリティのベストプラクティス
不要なワイルドカード「*」を使わない
開発時は便利ですが、本番環境では特定のドメインのみを許可するように設定しましょう:
❌ 避けるべき設定(SAM/CloudFormation)
Cors:
AllowOrigin: "'*'" # 本番環境では使用しない
❌ 避けるべき実装(Lambda)
'Access-Control-Allow-Origin': '*', # 本番環境では使用しない
複数オリジンを許可する必要がある場合
複数のドメインからのアクセスを許可する必要がある場合は、Lambda 関数で動的に対応します:
exports.handler = async (event) => {
// 許可するオリジンのリスト
const allowedOrigins = [
'https://d3rcrq5ytlkzie.cloudfront.net',
'https://www.example.com',
'https://app.example.com'
];
// リクエストの Origin ヘッダーを取得
const origin = event.headers.Origin || event.headers.origin || '';
// 許可されたオリジンかどうかチェック
const allowedOrigin = allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
const headers = {
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Content-Type': 'application/json',
};
// 以下、通常の処理...
};
クレデンシャル付きリクエストを許可する場合
Cookie や認証ヘッダーを含むリクエストを許可する場合は、追加設定が必要です:
const headers = {
'Access-Control-Allow-Origin': 'https://d3rcrq5ytlkzie.cloudfront.net',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Credentials': 'true', // クレデンシャル付きリクエストを許可
'Content-Type': 'application/json',
};
注意: Access-Control-Allow-Credentials: true
を使用する場合、Access-Control-Allow-Origin
に *
は使用できません。必ず具体的なドメインを指定してください。
🔧 トラブルシューティング
一般的な CORS エラーと解決策
エラーメッセージ | 考えられる原因 | 解決策 |
---|---|---|
Access to fetch at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. | APIレスポンスにCORSヘッダーがない | Lambdaですべてのレスポンスにヘッダーを含める |
Access to fetch at '...' from origin '...' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Method '...' is not allowed by Access-Control-Allow-Methods. | OPTIONSリクエストが正しく処理されていない | API GatewayとLambdaでOPTIONSメソッドを正しく設定 |
Access to fetch at '...' from origin '...' has been blocked by CORS policy: Request header field '...' is not allowed by Access-Control-Allow-Headers. | 指定したヘッダーが許可リストにない | Access-Control-Allow-Headers に必要なヘッダーを追加 |
Preflight response is not successful | OPTIONSリクエストがエラーを返している | API GatewayのOPTIONSハンドリングを確認 |
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include' | クレデンシャル付きリクエストでワイルドカードを使用 | 具体的なオリジンを指定 |
デバッグ手順
- ブラウザの開発者ツールでネットワークタブを確認
- OPTIONS リクエストが送信されているか
- レスポンスに正しい CORS ヘッダーが含まれているか
- CloudWatch ログで Lambda の動作を確認
- リクエストが Lambda に到達しているか
- エラーが発生していないか
- API Gateway のテスト機能を使用
- API Gateway コンソールからリクエストをテスト
- レスポンスヘッダーを確認
- curl でリクエストをテスト
curl -v -X OPTIONS \ -H "Origin: https://d3rcrq5ytlkzie.cloudfront.net" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Content-Type" \ https://your-api-gateway-id.execute-api.region.amazonaws.com/stage/path
📊 CORS 設定のチェックリスト
対策項目 | 必須? | 説明 |
---|---|---|
API Gateway に正しい CORS 設定 | ✅ | AllowOrigin に CloudFront ドメインを指定 |
Lambda 関数で CORS ヘッダー返却 | ✅ | すべてのレスポンスにヘッダーを含め、OPTIONS リクエストも処理 |
CloudFront で Origin ヘッダーを転送 | ✅ | 設定していないと CORS ヘッダーが正しく機能しない |
特定ドメインのみを許可 | ✅ | セキュリティ強化のため * を使用せず具体的なドメインを指定 |
クレデンシャル対応(必要な場合) | 選択 | Access-Control-Allow-Credentials: true を追加 |
複数オリジン対応(必要な場合) | 選択 | Lambda で動的に許可オリジンを制御 |
🚀 実装例:完全なソリューション
SAM テンプレート(template.yaml
)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: API with proper CORS configuration
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
AllowedOrigin:
Type: String
Default: 'https://d3rcrq5ytlkzie.cloudfront.net'
Description: 'Domain that will be allowed in CORS policy'
Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With'"
AllowOrigin: !Sub "'${AllowedOrigin}'"
MaxAge: "'600'"
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src/
Handler: app.handler
Runtime: nodejs18.x
Architectures:
- x86_64
Environment:
Variables:
ALLOWED_ORIGIN: !Ref AllowedOrigin
Events:
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /items
Method: ANY
Outputs:
ApiEndpoint:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/items"
FunctionName:
Description: "Lambda Function Name"
Value: !Ref MyFunction
Lambda 関数(src/app.js
)
exports.handler = async (event) => {
console.log('Event:', JSON.stringify(event));
// 環境変数から許可されたオリジンを取得
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'https://d3rcrq5ytlkzie.cloudfront.net';
// CORS ヘッダー
const headers = {
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Requested-With',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Content-Type': 'application/json',
};
// OPTIONS リクエスト(プリフライト)の処理
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// リクエストパラメータの取得
const queryParams = event.queryStringParameters || {};
const pathParams = event.pathParameters || {};
const body = event.body ? JSON.parse(event.body) : {};
// リクエストメソッドに応じた処理
let responseBody;
switch (event.httpMethod) {
case 'GET':
responseBody = {
message: 'GET request successful',
queryParams,
pathParams,
};
break;
case 'POST':
responseBody = {
message: 'POST request successful',
body,
};
break;
case 'PUT':
responseBody = {
message: 'PUT request successful',
body,
pathParams,
};
break;
case 'DELETE':
responseBody = {
message: 'DELETE request successful',
pathParams,
};
break;
default:
responseBody = {
message: `Unsupported method: ${event.httpMethod}`,
};
}
// 正常レスポンス
return {
statusCode: 200,
headers,
body: JSON.stringify(responseBody),
};
} catch (error) {
console.error('Error:', error);
// エラーレスポンス(CORS ヘッダーを含む)
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: 'Internal server error',
message: error.message,
}),
};
}
};
デプロイコマンド
# SAM テンプレートのビルド
sam build
# テンプレートのデプロイ
sam deploy --guided
# または、既存のパラメータを使用してデプロイ
sam deploy --parameter-overrides Environment=prod AllowedOrigin=https://www.example.com
📈 ROI と拡張性の考察
費用対効果
適切な CORS 設定は、開発の初期段階で実装することで以下のような効果が期待できます:
- 開発効率の向上: CORS エラーのデバッグに費やす時間を大幅に削減(平均約 4-8 時間の節約)
- セキュリティ強化: 許可されたオリジンからのアクセスのみを許可することでセキュリティリスクを低減
- インフラストラクチャ費用: 追加コストは発生せず、既存リソースの最適な構成のみ
拡張性
この CORS 設定アプローチは、以下のようなシナリオにも容易に対応できます:
- 複数環境(開発、テスト、本番)の管理: 環境変数やパラメータを使用して環境ごとに異なるオリジンを許可
- マルチテナント対応: Lambda 関数でテナントごとに許可するオリジンを動的に変更
- サードパーティ統合: 特定のパートナーサイトからのアクセスを許可する柔軟性
自動化と CI/CD パイプライン
GitHub Actions や AWS CodePipeline を使用して、以下のような自動化が可能です:
# GitHub Actions ワークフロー例(.github/workflows/deploy.yml)
name: Deploy SAM Application
on:
push:
branches:
- main
- develop
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Set environment variables
id: vars
run: |
if [[ ${{ github.ref }} == 'refs/heads/main' ]]; then
echo "ENV=prod" >> $GITHUB_OUTPUT
echo "ORIGIN=https://www.example.com" >> $GITHUB_OUTPUT
else
echo "ENV=dev" >> $GITHUB_OUTPUT
echo "ORIGIN=https://dev.example.com" >> $GITHUB_OUTPUT
fi
- name: Build SAM application
run: sam build
- name: Deploy SAM application
run: |
sam deploy --no-confirm-changeset \
--parameter-overrides Environment=${{ steps.vars.outputs.ENV }} AllowedOrigin=${{ steps.vars.outputs.ORIGIN }}
🎯 まとめ
AWS Lambda + API Gateway 構成での CORS 設定は、複数のコンポーネントに対して一貫した設定が必要です。本記事で説明した 4 つの対策をすべて実装することで、セキュアかつ適切に機能する API を構築できます。
特に重要なポイント:
- API Gateway の CORS 設定はテンプレートやコンソールで適切に設定する
- Lambda 関数ですべてのレスポンスに CORS ヘッダーを含める
- CloudFront から API Gateway へのリクエスト転送時に Origin ヘッダーを保持する
- セキュリティ強化のためワイルドカード(*)を避け、具体的なドメインを指定する
この手順に従うことで、CloudFront からのみアクセス可能な安全な API を構築でき、「CORS エラー」による開発の停滞を防ぐことができます。
コメント