1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon Bedrockを使って、S3のドキュメントに基づくAIの応答システムを構築してみました。(RAGアプリケーション簡易版)

Last updated at Posted at 2025-07-21

はじめに

Amazon Bedrockを使って、S3のドキュメントに基づくAIの応答システムを構築してみました。本記事では、以下の構成でWeb UIから質問を送信し、Claude Instantに回答させる仕組みを紹介します。

システム構成

[ User (ウェブブラウザ) ]
          │ ▲
          ▼ │
[ CloudFront (CDN: index.html, JS) ]
          │ ▲
          ▼ │
[ S3 (静的ホスティング + インプットファイル格納) ]
          │ ▲
          ▼ │
[ API Gateway (POSTリクエスト受信) ]
          │ ▲
          ▼ │
[ Lambda Function ]
    ├─ S3からインプットファイルを取得
    ├─ 内容をBedrockのモデルに渡す
    └─ Bedrockの応答を受け取り、ユーザーに返す
          │ ▲
          ▼ │
[ Amazon Bedrock (Claude Instant) ]

構築順序

1. API Gateway と Lambda の作成

ユーザーからのリクエストを処理するために、Lambda 関数を作成し、API Gateway(HTTP API)と統合します。
Lambda では、S3 からファイルを取得し、Bedrock に問い合わせて結果を返す処理を実装します。

2. CloudFront と S3 の構築

フロントエンドを配信するために、S3 に静的ホスティング用バケットを作成し、CloudFront を通じて公開します。
オリジンとして S3 を指定し、キャッシュ設定やオブジェクトへのアクセス権限も調整します。

3. S3 に index.html をアップロード

作成したフロントエンドファイル(例:index.html)を S3 にアップロードします。
正しい Content-Type を指定し、CloudFront のキャッシュが有効な場合は、必要に応じて Invalidation を行います。

4. S3 に import.txt をアップロード

ユーザーからの入力として処理する import.txt ファイルを S3 にアップロードします。
Lambda 関数はこのファイルを読み取り、Bedrock モデルに渡すことで応答を生成します。

5. Amazon Bedrock モデルの有効化

使用する Bedrock モデル(例:Claude Instant)をコンソールから有効化します。
併せて、Lambda から Bedrock にアクセスするための適切な IAM 権限を設定します。

1. API Gateway と Lambda の作成

以下テンプレートを元にCloudFormationのスタックを作成していきます。

CloudFormationで新規スタックを作成する方法の詳細は次の記事をご参照下さい。
CloudFormationで新規スタックを作成する方法

今回はYAMLで作成しておりますので、JSONに変換したい方は次の記事をご参照下さい。
YAMLからJSONに変換する方法

AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda + API Gateway with CORS for Bedrock Q&A


Parameters:
  S3BucketName:
    Type: String
    Description: Lambdaが読み込むS3バケット名
    Default: "auto-webui-web-bucket"

  S3ObjectKey:
    Type: String
    Description: Lambdaが読み込むS3のオブジェクトキー
    Default: "documents/import.txt"

Resources:
  QALambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: QALambdaFunction
      Runtime: python3.12
      Handler: index.lambda_handler
      Timeout: 300
      Role: !GetAtt LambdaExecutionRole.Arn
      Environment:
        Variables:
          BUCKET_NAME: !Ref S3BucketName
          OBJECT_KEY: !Ref S3ObjectKey
      Code:
        ZipFile: |
          import json
          import boto3
          import os

          s3 = boto3.client('s3')
          bedrock = boto3.client('bedrock-runtime')

          def lambda_handler(event, context):
              # CORS対応
              headers = {
                  "Access-Control-Allow-Origin": "*",
                  "Access-Control-Allow-Headers": "Content-Type",
                  "Access-Control-Allow-Methods": "OPTIONS,POST"
              }

              if event.get("httpMethod") == "OPTIONS":
                  return {
                      "statusCode": 200,
                      "headers": headers,
                      "body": json.dumps("CORS preflight OK")
                  }

              try:
                  # クライアント(Web UI)からの質問を取得
                  body = json.loads(event.get("body", "{}"))
                  question = body.get("question", "")

                  # 環境変数で設定したバケットとキーを取得
                  bucket = os.environ["BUCKET_NAME"]
                  key = os.environ["OBJECT_KEY"]

                  # S3から文書を読み込む
                  s3_obj = s3.get_object(Bucket=bucket, Key=key)
                  s3_text = s3_obj['Body'].read().decode('utf-8')

                  # Claude用のプロンプトフォーマット
                  prompt = f"""\n\nHuman:
          以下のドキュメントを参考に、ユーザーの質問に日本語で答えてください。

          --- ドキュメント ---
          {s3_text}

          --- 質問 ---
          {question}

          \n\nAssistant:"""

                  # Bedrock に送信(Claude Instant を想定)
                  response = bedrock.invoke_model(
                      modelId="anthropic.claude-instant-v1",
                      body=json.dumps({
                          "prompt": prompt,
                          "max_tokens_to_sample": 500,
                          "temperature": 0.7
                      }),
                      accept="application/json",
                      contentType="application/json"
                  )

                  result = json.loads(response['body'].read())

                  return {
                      "statusCode": 200,
                      "headers": headers,
                      "body": json.dumps({
                          "response": result.get("completion", "回答が得られませんでした")
                      })
                  }

              except Exception as e:
                  return {
                      "statusCode": 500,
                      "headers": headers,
                      "body": json.dumps({
                          "error": str(e)
                      })
                  }


  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: QALambdaExecutionRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: BedrockS3Policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"
              - Effect: Allow
                Action: s3:GetObject
                Resource: !Sub arn:aws:s3:::${S3BucketName}/${S3ObjectKey}
              - Effect: Allow
                Action: bedrock:InvokeModel
                Resource: "*"

  APIGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: QAApi

  APIResourceAsk:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref APIGateway
      ParentId: !GetAtt APIGateway.RootResourceId
      PathPart: ask

  APIAskPostMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref APIGateway
      ResourceId: !Ref APIResourceAsk
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub
          - arn:aws:apigateway:${Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
          - Region: !Ref "AWS::Region"
            LambdaArn: !GetAtt QALambdaFunction.Arn
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true

  APIAskOptionsMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref APIGateway
      ResourceId: !Ref APIResourceAsk
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        RequestTemplates:
          application/json: '{"statusCode": 200}'
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
              method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'"
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Methods: true

  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref QALambdaFunction
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${APIGateway}/*/*/ask

  Deployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - APIAskPostMethod
      - APIAskOptionsMethod
    Properties:
      RestApiId: !Ref APIGateway
      StageName: prod

Outputs:
  APIEndpoint:
    Description: "API Gateway Endpoint"
    Value: !Sub "https://${APIGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/ask"

パラメータの詳細

パラメータ名 用途 備考
S3BucketName Bedrockに受け渡すファイルが格納されるS3バケット名 2.で作成するスタック名+-web-bucketがS3バケット名となります。
S3ObjectKey Bedrockに受け渡すファイルが格納されるオブジェクトパス 例:documents/import.txt

2. CloudFront と S3 の構築

以下テンプレートを元にCloudFormationのスタックを作成していきます。

AWSTemplateFormatVersion: '2010-09-09'
Description: S3 + CloudFront

Parameters:
  ApiGatewayDomain:
    Type: String
    Description: APIGateway

Resources:
  WebSiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-web-bucket'
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  WebSiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebSiteBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowCloudFrontAccess
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: 's3:GetObject'
            Resource: !Sub '${WebSiteBucket.Arn}/*'
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'

  MyCustomOriginRequestPolicy:
    Type: AWS::CloudFront::OriginRequestPolicy
    Properties:
      OriginRequestPolicyConfig:
        Name: !Sub "${AWS::StackName}-CORSRequestPolicy"
        CookiesConfig:
          CookieBehavior: none
        HeadersConfig:
          HeaderBehavior: whitelist
          Headers:
            - Origin
            - Access-Control-Request-Method
            - Access-Control-Request-Headers
            - Content-Type
        QueryStringsConfig:
          QueryStringBehavior: all


  ## 2. CloudFront用 OAC(Origin Access Control)
  CloudFrontOAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: !Sub '${AWS::StackName}-OAC'
        Description: OAC for S3 access
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  ## 3. CloudFront ディストリビューション
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        DefaultRootObject: index.html
        Origins:
          - Id: S3Origin
            DomainName: !GetAtt WebSiteBucket.RegionalDomainName
            S3OriginConfig:
              OriginAccessIdentity: ''
            OriginAccessControlId: !Ref CloudFrontOAC

          - Id: APIGatewayOrigin
            DomainName: !Ref ApiGatewayDomain
            OriginPath: "/prod"
            CustomOriginConfig:
              OriginProtocolPolicy: https-only

        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6  # AWS-Managed CachingOptimized

        CacheBehaviors:
          - PathPattern: "ask"
            TargetOriginId: APIGatewayOrigin
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
              - PUT
              - POST
              - PATCH
              - DELETE
            CachedMethods:
              - GET
              - HEAD
              - OPTIONS
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad  # AWS-Managed CachingDisabled
            OriginRequestPolicyId: !Ref MyCustomOriginRequestPolicy  # AWS-Managed CORS-With-Preflight
            Compress: true

        ViewerCertificate:
          CloudFrontDefaultCertificate: true

Outputs:
  WebUIURL:
    Description: Web UI URL (CloudFront)
    Value: !Sub 'https://${CloudFrontDistribution.DomainName}'

パラメータの詳細

パラメータ名 用途 備考
ApiGatewayDomain Postリクエストを受信するAPI Gatewayのドメイン 1.で作成したスタックの出力の値を記載する。

3. S3 に index.html をアップロード

作成したS3に以下、index.htmlをアップロードする。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>質問フォーム</title>
  <style>
    body {
      font-family: sans-serif;
      padding: 2em;
      background-color: #f4f4f4;
    }
    input, button {
      padding: 0.5em;
      font-size: 1em;
      margin: 0.5em 0;
    }
    #responseArea {
      margin-top: 1em;
      padding: 1em;
      background-color: white;
      border-radius: 5px;
      box-shadow: 0 0 5px rgba(0,0,0,0.1);
      white-space: pre-wrap;
    }
  </style>
</head>
<body>
  <h1>質問を入力してください</h1>
  <input type="text" id="questionInput" placeholder="質問を入力" size="50" />
  <br />
  <button onclick="submitQuestion()">送信</button>
  <div id="responseArea">AIの回答がここに表示されます</div>

  <script>
    async function submitQuestion() {
      const question = document.getElementById("questionInput").value;
      const responseArea = document.getElementById("responseArea");

      if (!question.trim()) {
        responseArea.innerText = "質問を入力してください。";
        return;
      }

      responseArea.innerText = "送信中...";

      try {
        const response = await fetch("https://<API Gatewayのエンドポイント記載する>.execute-api.ap-northeast-1.amazonaws.com/prod/ask", {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({ question: question })
        });

        if (!response.ok) {
          throw new Error("APIからの応答が不正です");
        }

        const result = await response.json();

        responseArea.innerText = result.response || "返答がありませんでした。";

      } catch (err) {
        responseArea.innerText = "エラーが発生しました: " + err.message;
      }
    }
  </script>
</body>
</html>

4. S3 に import.txt をアップロード

index.htmlを配置したS3にフォルダ「document」を作成して、import.txtをアップロードします。
ここはAIが回答する際に考慮させたいテキストファイルをアップロードします。

記載例

CloudFormation記載ルール(cf_rules.txt)

1. テンプレート構成
----------------------
- AWSTemplateFormatVersion は常に先頭に記載
- Description を明確に記載(プロジェクト名、目的、作成者など)

2. Resourcesセクション
----------------------
- リソース名は 頭文字に z99project を付けて
- 複数の同種リソースがある場合は番号付きで区別(例: AppServer01, AppServer02)
- Type と Properties のインデントはスペース2つ

3. パラメータとマッピング
----------------------
- Parameters は Usage に応じて明確な説明を付与(Description 必須)
- Mappings は必要最低限にとどめ、コメントで用途を記載

4. Outputs
----------------------
- 他スタックで参照する値は Outputs に記載し Export 名も明示
- Description を必ず付与

5. 条件・依存関係
----------------------
- DependsOn を使用する場合は理由をコメントで記載
- Conditions は true/false のロジックが直感的にわかる命名を使用(例: IsProduction)

6. コメントの書き方
----------------------
- `#` を使用し、処理目的や背景を記載する

7. その他
----------------------
- YAML形式を推奨(可読性重視)
- セキュリティ関連リソース(IAM, KMS, SecurityGroup等)は別ファイルに分離可能
- スタック名、リソース名に環境名(dev, stg, prodなど)を含める

動作確認

CloudFrontのドメインにアクセスします。
入力フォームから好きな質問をしてみてください。

brave_screenshot_us-east-1.console.aws.amazon.com.png

image.png

まとめ

今回は、簡単なAI応答システムを作ってみました。
社内ルールをあらかじめまとめておけば、必要な情報をすぐに検索できるので、業務の効率アップに役立ちそうです。
また、社内メンバーの情報も入れておけば、AIを使った人事異動のサポートなんかもできるかもしれません。
気になるのはセキュリティ面ですが、対策としてはIAM認証やCognito、WAFなどでAPIへのアクセスを制限したり、必要に応じてIP制限やVPC連携を検討するのが良さそうです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?