こんにちは、クレスコの北垣です。 

以前、「生成AI時代のサーバレスAPI構成」 というタイトルで、ストリーミングレスポンスを実現するAWSサーバレスAPI構成について整理し記事にしました。今回はその中で紹介した CloudFront + Lambda Function URLs (Lambda Web Adapter利用) の構成でストリーミングレスポンスAPIをセキュアに実装する方法やパフォーマンスチューニングの手法について解説します。 

目次

  1. Lambda Web Adapter概要 
  2. アーキテクチャ概要 
  3. 実装 
  4. パフォーマンスチューニング 
  5. まとめ 

 

※ 注意 - 本記事の内容は2025年1月7日時点の情報で記載しております。 

1. Lambda Web Adapter概要 

Lambda Function URLs を利用してストリーミングレスポンスを実現する手法は、 以下の3パターンで利用可能です。(公式ドキュメントリンク) 

  • Node.js マネージドランタイム 
  • カスタムランタイム 
  • Lambda Web Adapter 

 

特に Lambda Web Adapter は、既存のWebアプリをほぼそのままLambda環境で実行可能にする 便利なツールです。 

 

Lambda Web Adapter の特徴 

  • 従来のウェブアプリケーションを、ほぼそのまま Lambda 上で動作させることができ、HTTPを使用するウェブサーバーアプリケーションに対応しています。対応しているフレームワークは以下の通りです。 
  1. Python: FastAPI, Flask 
  2. Node.js: Express.js, Next.js 
  3. Java: SpringBoot 
  4. Rust: Axum 
  5. Go: Gin 
  • Dockerfileに1行追加するだけで導入が完了します。そのため既存のWebアプリケーションコードの変更は最小限となります。ECSへの移行も容易です。 
     

Lambda Web Adapter の動作する仕組み 

Lambda Web AdapterはHTTPリクエストを受け取り、従来のWebサーバーアプリケーションが理解できる形に変換します。この変換により、通常のウェブフレームワークをサーバレス環境に適応させることが可能です。詳細は以下リンク先を参照ください。 

 

https://speakerdeck.com/tmokmss/aws-lambda-web-adapterwohuo-yong-suruxin-siisabaresunoshi-zhuang-patan 

2. アーキテクチャ概要

Lambda Function URLs だけでAPIを公開するにはセキュリティの面で不十分な場合があります。そのため、CloudFrontを前段に置き、他のAWSサービスと組み合わせてセキュリティを強化する構成を推奨します。 

 

以下、構成の例です。 

補足説明 

  • CloudfrontにWAFを統合することで、エッジロケーションでWAFルールが適用できオリジンサーバ(Lambda)に到達する前に攻撃を遮断可能(DDoS対策やIP制限も可能) 
  • Lambda@Edge / Cognitoを利用した認証認可、またはSecrets Managerを利用したAPIキー認証によるセキュリティの強化 
  • Lambda Function URLsのリソースポリシーを使用してCloudFrontからのアクセスのみを許可 

3. 実装

ここでは AWS Samplesで提供されているアプリをベース に、Lambda Web Adapterを利用したアプリケーションの構築を行う際に 注意すべき点にフォーカス して解説します。 

 

▼利用するAWS Samplesのリポジトリ 

https://github.com/aws-samples/bedrock-access-gateway 

※Amazon Bedrock 向け OpenAI 互換 RESTful APIを提供しているアプリです 

 

▼ AWS Lambda Web Adapterのリポジトリ 

https://github.com/awslabs/aws-lambda-web-adapter 

 

▼前提 

  • AWS環境構築には、ローカルの環境構築不要で利用可能な AWS Cloudshell を利用することをお勧めします。以降、Cloudshell利用前提で記載します。 
  • Amazon Bedrockの最新LLMを利用可能な us-east-1(バージニア北部) リージョンを利用します(利用するLLMは事前にアクセスできるようにしておきます。詳細は こちら を参照ください。) 
  • 以下の構成を構築します

 

(1) リポジトリをCloneし、Lambda Web Adapterを利用可能な定義に変更します。 

 

bedrock-access-gateway のリポジトリをCloneします。 

[cloudshell-user@ ~]$ git clone https://github.com/aws-samples/bedrock-access-gateway.git
[cloudshell-user@ ~]$ cd bedrock-access-gateway/
[cloudshell-user@ bedrock-access-gateway]$

Cloneしたら、定義を変更してきます。

 

対象ソース①:src/Dockerfile 

# 追加コード(2行目) [ Lambda Web Adapterを利用するための定義 ]
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 /lambda-adapter /opt/extensions/lambda-adapter

# 変更コード(10-11行目) [ Lambda Web Adapterを利用時の定義変更]
ENTRYPOINT ["uvicorn"]
CMD [ "api.app:app", "--host", "0.0.0.0", "--port", "8080"]

修正サンプル(画像右側が修正後): 

 

対象ソース②:src/api/app.py 

 

# コメントアウト(50行目) [ Mangumはストリーミングレスポンス非対応のため利用しない ]
#handler = Mangum(app)

修正サンプル(画像右側が修正後): 

 

(2) Dockerビルド実行しAWS ECRにイメージを登録します。 

 

シェルスクリプトが用意されていますが、Cloudshellで実行可能とするために、以下修正します。 

対象ソース③:scripts/push-to-ecr.sh 

# arm64の定義削除(12行目) [ Cloudshellはarm64のDockerビルドに非対応 ]
ARCHS=("amd64")

# コメントアウト(77行目) [ 今回はECS用イメージ不要 ]
#build_and_push_images "bedrock-proxy-api-ecs" "$TAG"

修正サンプル(画像右側が修正後): 

修正後、スクリプトを実行するとECRにコンテナイメージが登録されます。 

[cloudshell-user@ bedrock-access-gateway]$ cd scripts/
[cloudshell-user@ scripts]$ bash ./push-to-ecr.sh

 

(3) 認証用のAPIKeyをパラメータストアに登録します。 

 

APIKeyの値はご自身で決めたものを指定してください。 

[cloudshell-user@ scripts]$ aws ssm put-parameter --name "BedrockProxyAPIKey" --type "SecureString" --value "<APIKeyの値を決めて指定する>"

 

(4) Cloudformation templateコードを作成します。 

 

以下、Cloudformationコードを、deployment/clf-bedrock-proxy-api-custom-template.yamlとして登録します。 



# 注意事項(CloudFormationのtemplateコードについて)
# 1. templateコードはAWS CloudFormationのIaCジェネレータで自動生成されたものを、独自にカスタマイズして作成しています。
# 2. Lambda@Edge部分のPythonコードについては、以下のブログ記事で公開されていたNode.jsコードを参考にさせていただき、機能要件を満たすようにPythonで新たに実装したものです。
#    https://dev.classmethod.jp/articles/cloudfront-lambda-url-with-post-put-request/


AWSTemplateFormatVersion: '2010-09-09'
Description: 'Bedrock Proxy API with CloudFront Distribution and WAF'
Parameters:
  BastionIpAddress:
    Type: String
    Description: 'Bastion host IP address with /32 CIDR (e.g. 10.0.0.1/32)'
  ServiceName:
    Type: String
    Default: bedrock-proxy-api
Resources:
  # ===== IAM Roles and Policies =====
  LambdaBasicExecutionRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Lambda basic execution role policy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*
  IAMRoleBedrockProxyLambda:
    Type: AWS::IAM::Role
    Properties:
      Path: /service-role/
      ManagedPolicyArns:
        - !Ref LambdaBasicExecutionRolePolicy
        - arn:aws:iam::aws:policy/AmazonSSMFullAccess
        - arn:aws:iam::aws:policy/AmazonBedrockFullAccess
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
  IAMRoleEdgeHeaderControlLambda:
    Type: AWS::IAM::Role
    Properties:
      Path: /service-role/
      ManagedPolicyArns:
        - !Ref LambdaBasicExecutionRolePolicy
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - edgelambda.amazonaws.com
                - lambda.amazonaws.com
  # ===== Lambda Functions =====
  LambdaBedrockProxyFunction:
    Type: AWS::Lambda::Function
    DependsOn: IAMRoleBedrockProxyLambda
    Properties:
      MemorySize: 512
      Timeout: 60
      Code:
        ImageUri: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/${ServiceName}:latest
      Role: !GetAtt IAMRoleBedrockProxyLambda.Arn
      FunctionName: !Ref ServiceName
      PackageType: Image
      LoggingConfig:
        LogFormat: Text
        LogGroup: !Sub /aws/lambda/${ServiceName}
      Environment:
        Variables:
          API_KEY_PARAM_NAME: BedrockProxyAPIKey
          DEBUG: false
          DEFAULT_MODEL: us.anthropic.claude-3-5-sonnet-20241022-v2:0
          DEFAULT_EMBEDDING_MODEL: cohere.embed-multilingual-v3
          ENABLE_CROSS_REGION_INFERENCE: true
          AWS_LWA_ASYNC_INIT: true
          AWS_LWA_INVOKE_MODE: response_stream
          AWS_LWA_AUTHORIZATION_SOURCE: authorization-custom
      EphemeralStorage:
        Size: 512
      Architectures:
        - x86_64
  LambdaEdgeHeaderControlFunction:
    Type: AWS::Lambda::Function
    DependsOn: IAMRoleEdgeHeaderControlLambda
    Properties:
      MemorySize: 128
      Timeout: 5
      Handler: index.lambda_handler
      Role: !GetAtt IAMRoleEdgeHeaderControlLambda.Arn
      FunctionName: edge-HeaderControl-for-lambda
      Runtime: python3.13
      Code:
        ZipFile: |
          import hashlib
          import base64
          def lambda_handler(event, context):
              request = event['Records'][0]['cf']['request']
              headers = request['headers']
              # x-amz-content-sha256ヘッダーへのペイロードハッシュ値登録
              body = ''
              if 'body' in request:
                  encoding = request['body'].get('encoding', '')
                  data = request['body'].get('data', '')
                  if encoding == 'base64':
                      body = base64.b64decode(data).decode('utf-8')
                  else:
                      body = data
              sha256_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()
              headers['x-amz-content-sha256'] = [{
                  'key': 'x-amz-content-sha256',
                  'value': sha256_hash
              }]
              # authorization header replace for Lambda-web-adapter
              if 'authorization' in headers:
                  headers['authorization-custom'] = [{
                      'key': 'authorization-custom',
                      'value': headers['authorization'][0]['value']
                  }]
                  del headers['authorization']
              return request
      LoggingConfig:
        LogFormat: Text
        LogGroup: /aws/lambda/edge-HeaderControl-for-lambda
      EphemeralStorage:
        Size: 512
      Architectures:
        - x86_64
  # ===== Lambda Versions and Permissions =====
  LambdaVersionfunctionedgeHeaderControlforlambda:
    Type: AWS::Lambda::Version
    DependsOn: LambdaEdgeHeaderControlFunction
    Properties:
      FunctionName: !Ref LambdaEdgeHeaderControlFunction
      Description: Version published for Lambda@Edge
      RuntimePolicy:
        UpdateRuntimeOn: Auto
  LambdaUrlBedrockProxyFunction:
    Type: AWS::Lambda::Url
    DependsOn: LambdaBedrockProxyFunction
    Properties:
      AuthType: AWS_IAM
      InvokeMode: RESPONSE_STREAM
      TargetFunctionArn: !GetAtt LambdaBedrockProxyFunction.Arn
  LambdaPermissionBedrockProxyFunction:
    Type: AWS::Lambda::Permission
    DependsOn: LambdaBedrockProxyFunction
    Properties:
      FunctionName: !GetAtt LambdaBedrockProxyFunction.Arn
      Action: lambda:InvokeFunctionUrl
      Principal: cloudfront.amazonaws.com
      SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionBedrockProxy}
  LambdaPermissionfunctionedgeHeaderControlLambda:
    Type: AWS::Lambda::Permission
    DependsOn: LambdaVersionfunctionedgeHeaderControlforlambda
    Properties:
      FunctionName: !Ref LambdaVersionfunctionedgeHeaderControlforlambda
      Action: lambda:GetFunction
      Principal: replicator.lambda.amazonaws.com
  # ===== WAF Resources =====
  WAFv2IPSet00CLOUDFRONTBedrockProxy:
    Type: AWS::WAFv2::IPSet
    Properties:
      Addresses:
        - !Ref BastionIpAddress
      Description: IP restriction for API access
      IPAddressVersion: IPV4
      Scope: CLOUDFRONT
      Name: !Sub ${AWS::StackName}-allowed-ips
  WAFv2WebACLCLOUDFRONTBedrockProxy:
    Type: AWS::WAFv2::WebACL
    DependsOn: WAFv2IPSet00CLOUDFRONTBedrockProxy
    Properties:
      Description: WAF rules for Bedrock Proxy API
      DefaultAction:
        Allow: {}
      Scope: CLOUDFRONT
      Rules:
        - Action:
            Allow: {}
          Name: ipRestrict
          Priority: 0
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFv2IPSet00CLOUDFRONTBedrockProxy.Arn
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: ipRestrict
        - Name: AWS-AWSManagedRulesAmazonIpReputationList
          Priority: 1
          OverrideAction:
            None: {}
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesAmazonIpReputationList
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: AWS-AWSManagedRulesAmazonIpReputationList
        - Name: AWS-AWSManagedRulesCommonRuleSet
          Priority: 2
          OverrideAction:
            None: {}
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesCommonRuleSet
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: AWS-AWSManagedRulesCommonRuleSet
        - Name: AWS-AWSManagedRulesKnownBadInputsRuleSet
          Priority: 3
          OverrideAction:
            None: {}
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesKnownBadInputsRuleSet
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: AWS-AWSManagedRulesKnownBadInputsRuleSet
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: !Sub ${ServiceName}-webacl-metrics
  # ===== CloudFront Resources =====
  CloudFrontOACBedrockProxy:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: oac-for-lambda-url
        SigningBehavior: always
        SigningProtocol: sigv4
        OriginAccessControlOriginType: lambda
  CloudFrontDistributionBedrockProxy:
    Type: AWS::CloudFront::Distribution
    DependsOn:
      - LambdaUrlBedrockProxyFunction
      - WAFv2WebACLCLOUDFRONTBedrockProxy
      - CloudFrontOACBedrockProxy
    Properties:
      DistributionConfig:
        Enabled: true
        Comment: Distribution for Bedrock Proxy API
        DefaultRootObject: ""
        IPV6Enabled: true
        HttpVersion: http2
        Origins:
          - DomainName: !Select [2, !Split ["/", !GetAtt LambdaUrlBedrockProxyFunction.FunctionUrl]] 
            Id: CloudfrontToLambda
            ConnectionTimeout: 10
            ConnectionAttempts: 3
            OriginAccessControlId: !Ref CloudFrontOACBedrockProxy
            CustomOriginConfig:
              OriginProtocolPolicy: https-only
              OriginSSLProtocols: [TLSv1.2]
              OriginKeepaliveTimeout: 5
              OriginReadTimeout: 60
        DefaultCacheBehavior:
          TargetOriginId: CloudfrontToLambda
          ViewerProtocolPolicy: allow-all
          Compress: true
          LambdaFunctionAssociations:
            - EventType: origin-request
              IncludeBody: true
              LambdaFunctionARN: !Ref LambdaVersionfunctionedgeHeaderControlforlambda
          AllowedMethods:
            - HEAD
            - DELETE
            - POST
            - GET
            - OPTIONS
            - PUT
            - PATCH
          CachedMethods:
            - HEAD
            - GET
          CachePolicyId: !Ref CloudFrontCachePolicyBedrockProxy
          OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicyBedrockProxy
        WebACLId: !GetAtt WAFv2WebACLCLOUDFRONTBedrockProxy.Arn
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
          MinimumProtocolVersion: TLSv1
        PriceClass: PriceClass_All
  CloudFrontCachePolicyBedrockProxy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: Custom-CachingDisabled
        Comment: Policy with caching disabled
        DefaultTTL: 0
        MaxTTL: 0
        MinTTL: 0
        ParametersInCacheKeyAndForwardedToOrigin:
          QueryStringsConfig:
            QueryStringBehavior: none
          HeadersConfig:
            HeaderBehavior: none
          CookiesConfig:
            CookieBehavior: none
          EnableAcceptEncodingGzip: false
          EnableAcceptEncodingBrotli: false
  CloudFrontOriginRequestPolicyBedrockProxy:
    Type: AWS::CloudFront::OriginRequestPolicy
    Properties:
      OriginRequestPolicyConfig:
        Name: Custom-AllViewerExceptHostHeader
        Comment: Policy to forward all parameters in viewer requests except for the Host header
        QueryStringsConfig:
          QueryStringBehavior: all
        HeadersConfig:
          HeaderBehavior: allExcept
          Headers:
            - host
        CookiesConfig:
          CookieBehavior: all
Outputs:
  ApiEndpoint:
    Description: API Endpoint URL
    Value: !Sub https://${CloudFrontDistributionBedrockProxy.DomainName}/api/v1


重要な点、ハマりどころについて解説した図がこちらです。 

 

(5) Cloudformationを実行します。 

 

実行パラメータにアクセスを許可するIPアドレスを指定してください。 

[cloudshell-user@ bedrock-access-gateway]$ aws cloudformation create-stack --stack-name BedrockProxyStack --template-body file://./deployment/clf-bedrock-proxy-api-custom-template.yaml --parameters ParameterKey=BastionIpAddress,ParameterValue="<許可するIPアドレスを指定してください>/32" --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND

※7分程度でデプロイが完了します  

完了後、出力リソースを確認し、APIエンドポイントを確認します。 

[cloudshell-user@ bedrock-access-gateway]$ aws cloudformation describe-stacks --stack-name BedrockProxyStack --query "Stacks[0].Outputs"
[
    {
        "OutputKey": "ApiEndpoint",
        "OutputValue": "https://
xxxxxxxxxxxxxxxxxx.cloudfront.net/api/v1",
        "Description": "API Endpoint URL"
    }
]
[cloudshell-user@ bedrock-access-gateway]$

これでAWSの環境構築は完了です。 

 

(6) コード生成AIサービスから利用してみる 

オープンソースのコード生成AIツール「Continue.dev」を使用し、本記事で作成したAPIに接続させてみます。(Continue.devの概要や利用方法については 公式サイト をご参照ください。) 

 

VS CodeにContinue.dev拡張機能を導入し、設定Jsonファイルを定義します。 

{
    "models": [
        {
            "model": "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
            "contextLength": 128000,
            "title": "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
            "systemMessage": "You are an expert in software development. You give kind and concise answers in Japanese.",
            "provider": "openai",
            "apiKey":
"<ご自身で指定したAPIキーの値>",
            "apiBase": "https://
<ここに発行されたCloudfrontドメインを指定する>.cloudfront.net/api/v1",
            "useLegacyCompletionsEndpoint": false,
            "completionOptions": {
                "temperature": 1,
                "maxTokens": 4096,
               "stream": true
            }
        }
    ]
}

 

あとは利用するだけです。 

 

 

画像だと伝わりませんが、ストリーミングレスポンスで回答が画面に表示されます。

マルチモーダルの問い合わせも可能ですし、プログラムに関係ない通常のチャットツールとしても利用可能です。

4. パフォーマンスチューニング

Lambda Web Adapterを利用すると、コールドスタートの影響で性能が思ったほど出ないことがあります。Lambdaのコールドスタート対策では、Provisioned Concurrency を利用する または SnapStart を利用する の2パターンが主流かと思います。 

SnapStartは特定のランタイムのみサポートされているため、コンテナイメージを利用している場合はProvisioned Concurrencyを利用して対策します。 

 

※注意 

Provisioned Concurrencyは無料枠が無く、設定内容に応じてコストが掛かる点にご注意ください。 

 

設定方法や注意点など、Provisioned Concurrencyの詳細について以下リンク先を参照ください。 

https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html 

5. まとめ

Lambda Web Adapterは、従来のWebアプリケーションをAWSのサーバレス環境へ容易に移行する強力なツールです。本記事で紹介したアーキテクチャと実装方法を活用することで、以下の利点が得られます: 

 

(1) 簡単な移行:  

既存のWebアプリケーションをほぼそのままLambda上で動作させることができます。 

(2) セキュリティの強化:  

CloudFrontとWAFを組み合わせることで、エッジでの防御が可能になり、DDoS攻撃やIPアドレス制限などの対策が実現できます。 

(3) パフォーマンスの最適化:  

Provisioned Concurrencyを利用することで、コールドスタートの問題を軽減し、応答性能を向上させることができます。 

(4) 柔軟性:  

Amazon Bedrockなどの最新のAWSサービスとの統合が容易になり、AIを活用したアプリケーションの開発が可能です。 

(5) コスト効率:  

サーバーレスアーキテクチャにより、使用した分だけ課金されるため、リソースの無駄を削減できます。 

 

この構成は、特にAI機能を備えたWebアプリケーションや、高いセキュリティが要求されるAPIの構築に適しています。ただし、Provisioned Concurrencyのコストには注意が必要です。 

今後のクラウド開発において、このようなサーバーレスアーキテクチャの活用はますます重要になると考えられます。本記事の内容が参考になれば幸いです。