PR

AWS Lambda + API Gateway で CORS 制限を正しく設定する完全ガイド

🎯 概要

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 の流れ

  1. ブラウザが異なるオリジンへのリクエストを検出
  2. 実際のリクエスト前に プリフライトリクエスト (OPTIONS メソッド) を送信
  3. サーバーが適切な CORS ヘッダーで応答
  4. ブラウザが応答を評価し、許可されていれば実際のリクエストを実行

✅ 対策① — 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 コンソールでの設定

  1. API Gateway コンソールを開く
  2. API を選択 → リソースを選択
  3. アクション → CORS の有効化
  4. 以下の設定を入力:
    • 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
  5. 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 ヘッダーを転送」する手順

  1. CloudFront コンソールにアクセス
  2. ディストリビューションを選択 → 「ビヘイビア」タブを開く
  3. API Gateway 向けのビヘイビアを選択または作成
  4. 以下のいずれかの方法で設定:

方法1: キャッシュポリシーを使用(推奨)

  1. 「キャッシュポリシー」を「CachingDisabled」または「CORS-S3Origin」に設定
  2. または、カスタムキャッシュポリシーを作成し、「Origin」ヘッダーをキャッシュキーに含める

方法2: レガシーキャッシュ設定の場合

  1. 「レガシーキャッシュ設定」を選択
  2. 「オリジンへの転送ヘッダー」で「すべてのビューワーヘッダー」を選択 または
  3. 「ホワイトリストされたヘッダー」を選択し、「Origin」を追加

方法3: オリジンリクエストポリシーを使用

  1. 「オリジンリクエストポリシー」で「CORS-CustomOrigin」または「AllViewer」を選択
  2. または、カスタムオリジンリクエストポリシーを作成し、「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 successfulOPTIONSリクエストがエラーを返している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'クレデンシャル付きリクエストでワイルドカードを使用具体的なオリジンを指定

デバッグ手順

  1. ブラウザの開発者ツールでネットワークタブを確認
    • OPTIONS リクエストが送信されているか
    • レスポンスに正しい CORS ヘッダーが含まれているか
  2. CloudWatch ログで Lambda の動作を確認
    • リクエストが Lambda に到達しているか
    • エラーが発生していないか
  3. API Gateway のテスト機能を使用
    • API Gateway コンソールからリクエストをテスト
    • レスポンスヘッダーを確認
  4. 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 設定は、開発の初期段階で実装することで以下のような効果が期待できます:

  1. 開発効率の向上: CORS エラーのデバッグに費やす時間を大幅に削減(平均約 4-8 時間の節約)
  2. セキュリティ強化: 許可されたオリジンからのアクセスのみを許可することでセキュリティリスクを低減
  3. インフラストラクチャ費用: 追加コストは発生せず、既存リソースの最適な構成のみ

拡張性

この CORS 設定アプローチは、以下のようなシナリオにも容易に対応できます:

  1. 複数環境(開発、テスト、本番)の管理: 環境変数やパラメータを使用して環境ごとに異なるオリジンを許可
  2. マルチテナント対応: Lambda 関数でテナントごとに許可するオリジンを動的に変更
  3. サードパーティ統合: 特定のパートナーサイトからのアクセスを許可する柔軟性

自動化と 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 を構築できます。

特に重要なポイント:

  1. API Gateway の CORS 設定はテンプレートやコンソールで適切に設定する
  2. Lambda 関数ですべてのレスポンスに CORS ヘッダーを含める
  3. CloudFront から API Gateway へのリクエスト転送時に Origin ヘッダーを保持する
  4. セキュリティ強化のためワイルドカード(*)を避け、具体的なドメインを指定する

この手順に従うことで、CloudFront からのみアクセス可能な安全な API を構築でき、「CORS エラー」による開発の停滞を防ぐことができます。

📚 参考リソース

コメント

タイトルとURLをコピーしました