React で Amazon Bedrock Knowledge Bases ベースの簡易 RAG チャットボットをつくる [2026年1月版] 実装編

こんにちは、広野です。

AWS re:Invent 2025 で、Amazon S3 Vectors が GA されました。

以前は RAG 用のベクトルデータベースとして Amazon OpenSearch Service や Amazon Aurora など高額なデータベースサービスを使用しなければならなかったのですが、Amazon S3 ベースで安価に気軽に RAG 環境を作成できるようになったので嬉しいです。

それを受けて、以前作成した RAG チャットボット環境をアレンジしてみました。

内容が多いので、記事を 3つに分けます。本記事は実装編です。

つくったもの

こんな感じの RAG チャットボットです。

  • 画面から問い合わせすると、回答が文字列細切れのストリームで返ってきます。それらをアプリ側で結合して、順次画面に表示しています。
  • 回答の途中で、AI が回答生成の根拠にしたドキュメントの情報が送られてくることがあるので、あればそのドキュメント名を表示します。一般的には親切にドキュメントへのリンクも付いていると思うのですが、今回は簡略化のため省略しました。
  • このサンプル環境では、AWS が提供している AWS サービス別資料 の PDF を独自ドキュメントとして読み込ませ、回答生成に使用させています。

前回の記事

本記事はアーキテクチャ概要編の続編記事です。以下の記事をお読みの上、本記事にお戻りください。

 

アーキテクチャ

  • 図の左側半分、アプリ UI 基盤は以下の記事と全く同じです。お手数ですが内容についてはこちらをご覧ください。
  • 図の右側半分、Lambda 関数から Bedrock ナレッジベースに問い合わせるところを今回新たに作成しています。
  • まず、RAG に必要な独自ドキュメントを用意し、ドキュメント用 S3 バケットに保存します。
  • S3 Vectors を使用して、Vector バケットとインデックスを作成します。
  • これら S3 リソースを Amazon Bedrock Knowledge Bases でナレッジベースとして関連付けます。
  • それができると、ドキュメント用 S3 バケットのドキュメント内容をベクトルデータに変換して S3 Vectors に保存してくれます。これを埋め込み (Embedding) と言います。埋め込みに使用する AI モデルは Amazon Titan Text Embeddings V2 を使用します。
  • Bedrock ナレッジベースが完成すると、それを使用して回答を生成する LLM を指定します。今回は Amazon Nova 2 Lite を使用します。Lambda 関数内でパラメータとして指定して、プロンプトとともに問い合わせることになります。
  • フロントエンドの UI は React で開発します。

 

実装について

まず、この基盤のデプロイについては以下の Amazon Bedrock 生成 AI チャットボットの環境がデプロイできていることが前提になっています。そこで紹介されている基盤に機能追加したイメージです。

 

リソースは基本 AWS CloudFormation でデプロイしています。上記に続く、追加分のテンプレートをこちらに記載します。インラインで説明をコメントします。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a S3 vector bucket and index as a RAG Knowledge base.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SystemName:
    Type: String
    Description: System name. use lower case only. (e.g. example)
    Default: example
    MaxLength: 10
    MinLength: 1

  SubName:
    Type: String
    Description: System sub name. use lower case only. (e.g. prod or dev)
    Default: dev
    MaxLength: 10
    MinLength: 1

  DomainName:
    Type: String
    Description: Domain name for URL. xxxxx.xxx (e.g. example.com)
    Default: example.com
    AllowedPattern: "[^\\s@]+\\.[^\\s@]+"

  SubDomainName:
    Type: String
    Description: Sub domain name for URL. (e.g. example-prod or example-dev)
    Default: example-dev
    MaxLength: 20
    MinLength: 1

  Dimension:
    Type: Number
    Description: The dimensions of the vectors to be inserted into the vector index. The value depends on the embedding model.
    Default: 1024
    MaxValue: 4096
    MinValue: 1

  EmbeddingModelId:
    Type: String
    Description: The embedding model ID.
    Default: amazon.titan-embed-text-v2:0
    MaxLength: 100
    MinLength: 1

  LlmModelId:
    Type: String
    Description: The LLM model ID for the Knowledge base.
    Default: global.amazon.nova-2-lite-v1:0
    MaxLength: 100
    MinLength: 1

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Configuration"
        Parameters:
          - SystemName
          - SubName
      - Label:
          default: "Domain Configuration"
        Parameters:
          - DomainName
          - SubDomainName
      - Label:
          default: "Embedding Configuration"
        Parameters:
          - Dimension
          - EmbeddingModelId
      - Label:
          default: "Knowledge Base Configuration"
        Parameters:
          - LlmModelId

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
# ドキュメント保存用 S3 汎用バケット
  S3BucketKbDatasource:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${SystemName}-${SubName}-kbdatasource
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders:
              - "*"
            AllowedMethods:
              - "GET"
              - "HEAD"
              - "PUT"
              - "POST"
              - "DELETE"
            AllowedOrigins:
              - !Sub https://${SubDomainName}.${DomainName}
            ExposedHeaders:
              - last-modified
              - content-type
              - content-length
              - etag
              - x-amz-version-id
              - x-amz-request-id
              - x-amz-id-2
              - x-amz-cf-id
              - x-amz-storage-class
              - date
              - access-control-expose-headers
            MaxAge: 3000
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}

  S3BucketPolicyKbDatasource:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketKbDatasource
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action: "s3:*"
            Effect: Deny
            Resource:
              - !Sub "arn:aws:s3:::${S3BucketKbDatasource}"
              - !Sub "arn:aws:s3:::${S3BucketKbDatasource}/*"
            Condition:
              Bool:
                "aws:SecureTransport": "false"
            Principal: "*"
    DependsOn:
      - S3BucketKbDatasource

  # S3 Vector バケット
  S3VectorBucket:
    Type: AWS::S3Vectors::VectorBucket
    Properties:
      VectorBucketName: !Sub ${SystemName}-${SubName}-vectordb

  # S3 Vector バケットに関連付けるインデックス
  S3VectorBucketIndex:
    Type: AWS::S3Vectors::Index
    Properties:
      IndexName: !Sub ${SystemName}-${SubName}-vectordb-index
      DataType: float32
      Dimension: !Ref Dimension
      DistanceMetric: cosine
      VectorBucketArn: !GetAtt S3VectorBucket.VectorBucketArn
      MetadataConfiguration:
        NonFilterableMetadataKeys:
          - AMAZON_BEDROCK_TEXT
          - AMAZON_BEDROCK_METADATA
    DependsOn:
      - S3VectorBucket

# ------------------------------------------------------------#
# Bedrock Knowledge Base
# ------------------------------------------------------------#
  # ここで、各種 S3 を 1つの Knowledge Base として関連付ける
  BedrockKnowledgeBase:
    Type: AWS::Bedrock::KnowledgeBase
    Properties:
      Name: !Sub ${SystemName}-${SubName}-kb
      Description: !Sub RAG Knowledge Base for ${SystemName}-${SubName}
      KnowledgeBaseConfiguration:
        Type: VECTOR
        VectorKnowledgeBaseConfiguration:
          EmbeddingModelArn: !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/${EmbeddingModelId}
      RoleArn: !GetAtt IAMRoleBedrockKb.Arn
      StorageConfiguration:
        Type: S3_VECTORS
        S3VectorsConfiguration:
          IndexArn: !GetAtt S3VectorBucketIndex.IndexArn
          VectorBucketArn: !GetAtt S3VectorBucket.VectorBucketArn
      Tags:
        Cost: !Sub ${SystemName}-${SubName}
    DependsOn:
      - IAMRoleBedrockKb

  # ドキュメント保存用 S3 バケットはここで DataSource として関連付けないと機能しない
  BedrockKnowledgeBaseDataSource:
    Type: AWS::Bedrock::DataSource
    Properties:
      Name: !Sub ${SystemName}-${SubName}-kb-datasource
      Description: !Sub RAG Knowledge Base Data Source for ${SystemName}-${SubName}
      KnowledgeBaseId: !Ref BedrockKnowledgeBase
      DataDeletionPolicy: RETAIN
      DataSourceConfiguration:
        Type: S3
        S3Configuration:
          BucketArn: !GetAtt S3BucketKbDatasource.Arn
    DependsOn:
      - S3BucketKbDatasource
      - BedrockKnowledgeBase

# ------------------------------------------------------------#
# AppSync Events
# ------------------------------------------------------------#
  AppSyncChannelNamespaceRagSR:
    Type: AWS::AppSync::ChannelNamespace
    Properties:
      Name: rag-stream-response
      ApiId:
        Fn::ImportValue:
          !Sub AppSyncApiId-${SystemName}-${SubName}
      CodeHandlers: |
        import { util } from '@aws-appsync/utils';
        export function onSubscribe(ctx) {
          const requested = ctx.info.channel.path;
          if (!requested.startsWith(`/rag-stream-response/${ctx.identity.sub}`)) {
            util.unauthorized();
          }
        }
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}

# ------------------------------------------------------------#
# API Gateway REST API
# ------------------------------------------------------------#
  RestApiRagSR:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub rag-sr-${SystemName}-${SubName}
      Description: !Sub REST API to call Lambda rag-stream-response-${SystemName}-${SubName}
      EndpointConfiguration:
        Types:
          - REGIONAL
        IpAddressType: dualstack
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}

  RestApiDeploymentRagSR:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref RestApiRagSR
    DependsOn:
      - RestApiMethodRagSRPost
      - RestApiMethodRagSROptions

  RestApiStageRagSR:
    Type: AWS::ApiGateway::Stage
    Properties:
      StageName: prod
      Description: production stage
      RestApiId: !Ref RestApiRagSR
      DeploymentId: !Ref RestApiDeploymentRagSR
      MethodSettings:
        - ResourcePath: "/*"
          HttpMethod: "*"
          LoggingLevel: INFO
          DataTraceEnabled : true
      TracingEnabled: false
      AccessLogSetting:
        DestinationArn: !GetAtt LogGroupRestApiRagSR.Arn
        Format: '{"requestId":"$context.requestId","status":"$context.status","sub":"$context.authorizer.claims.sub","email":"$context.authorizer.claims.email","resourcePath":"$context.resourcePath","requestTime":"$context.requestTime","sourceIp":"$context.identity.sourceIp","userAgent":"$context.identity.userAgent","apigatewayError":"$context.error.message","authorizerError":"$context.authorizer.error","integrationError":"$context.integration.error"}'
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}

  RestApiAuthorizerRagSR:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Name: !Sub restapi-authorizer-ragsr-${SystemName}-${SubName}
      RestApiId: !Ref RestApiRagSR
      Type: COGNITO_USER_POOLS
      ProviderARNs:
        - Fn::ImportValue:
            !Sub CognitoArn-${SystemName}-${SubName}
      AuthorizerResultTtlInSeconds: 300
      IdentitySource: method.request.header.Authorization

  RestApiResourceRagSR:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApiRagSR
      ParentId: !GetAtt RestApiRagSR.RootResourceId
      PathPart: ragsr

  RestApiMethodRagSRPost:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApiRagSR
      ResourceId: !Ref RestApiResourceRagSR
      HttpMethod: POST
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref RestApiAuthorizerRagSR
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Credentials:
          Fn::ImportValue:
            !Sub ApigLambdaInvocationRoleArn-${SystemName}-${SubName}
        Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaRagSR.Arn}/invocations"
        PassthroughBehavior: NEVER
        RequestTemplates:
          application/json: |
            {
              "body": $input.json('$'),
              "sub": "$context.authorizer.claims.sub"
            }
        RequestParameters:
          integration.request.header.X-Amz-Invocation-Type: "'Event'"
        IntegrationResponses:
          - ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
            ResponseTemplates:
              application/json: ''
            StatusCode: '202'
      MethodResponses:
        - StatusCode: '202'
          ResponseModels:
            application/json: Empty
          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

  RestApiMethodRagSROptions:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApiRagSR
      ResourceId: !Ref RestApiResourceRagSR
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        Credentials:
          Fn::ImportValue:
            !Sub ApigLambdaInvocationRoleArn-${SystemName}-${SubName}
        IntegrationResponses:
          - ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
            ResponseTemplates:
              application/json: ''
            StatusCode: '200'
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
      MethodResponses:
        - ResponseModels:
            application/json: Empty
          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
          StatusCode: '200'

# ------------------------------------------------------------#
# API Gateway LogGroup (CloudWatch Logs)
# ------------------------------------------------------------#
  LogGroupRestApiRagSR:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/apigateway/${RestApiRagSR}
      RetentionInDays: 365
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}

# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
  LambdaRagSR:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub rag-sr-${SystemName}-${SubName}
      Description: !Sub Lambda Function to invoke Bedrock Knowledge Bases for ${SystemName}-${SubName}
      Architectures:
        - x86_64
      Runtime: python3.14
      Timeout: 300
      MemorySize: 128
      Environment:
        Variables:
          APPSYNC_API_ENDPOINT:
            Fn::ImportValue:
              !Sub AppSyncEventsEndpointHttp-${SystemName}-${SubName}
          MODEL_ARN: !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/${LlmModelId}"
          KNOWLEDGE_BASE_ID: !Ref BedrockKnowledgeBase
          REGION: !Ref AWS::Region
      Role: !GetAtt LambdaBedrockKbRole.Arn
      Handler: index.lambda_handler
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
      Code:
        ZipFile: |
          import os
          import json
          import boto3
          import urllib.request
          from botocore.auth import SigV4Auth
          from botocore.awsrequest import AWSRequest
          # common objects and valiables
          session = boto3.session.Session()
          bedrock_agent = boto3.client('bedrock-agent-runtime')
          endpoint = os.environ['APPSYNC_API_ENDPOINT']
          model_arn = os.environ['MODEL_ARN']
          knowledge_base_id = os.environ['KNOWLEDGE_BASE_ID']
          region = os.environ['REGION']
          service = 'appsync'
          headers = {'Content-Type': 'application/json'}
          # AppSync publish message function
          def publish_appsync_message(sub, appsync_session_id, payload, credentials):
            body = json.dumps({
              "channel": f"rag-stream-response/{sub}/{appsync_session_id}",
              "events": [ json.dumps(payload) ]
            }).encode("utf-8")
            aws_request = AWSRequest(
              method='POST',
              url=endpoint,
              data=body,
              headers=headers
            )
            SigV4Auth(credentials, service, region).add_auth(aws_request)
            req = urllib.request.Request(
              url=endpoint,
              data=aws_request.body,
              method='POST'
            )
            for k, v in aws_request.headers.items():
              req.add_header(k, v)
            with urllib.request.urlopen(req) as res:
              return res.read().decode('utf-8')
          # handler
          def lambda_handler(event, context):
            try:
              credentials = session.get_credentials().get_frozen_credentials()
              # API Gateway からのインプットを取得
              prompt = event['body']['prompt']
              # セッション ID はアーキテクチャ概要編で説明した通り2種類ある
              appsync_session_id = event['body']['appsyncSessionId']
              bedrock_session_id = event['body'].get('bedrockSessionId')
              sub = event['sub']
              # Amazon Bedrock Knowledge Bases への問い合わせパラメータ作成
              request = {
                "input": {
                  "text": prompt
                },
                "retrieveAndGenerateConfiguration": {
                  "type": "KNOWLEDGE_BASE",
                  "knowledgeBaseConfiguration": {
                    "knowledgeBaseId": knowledge_base_id,
                    "modelArn": model_arn,
                    "generationConfiguration": {
                      "inferenceConfig": {
                        "textInferenceConfig": {
                          "maxTokens": 10000,
                          "temperature": 0.5,
                          "topP": 0.9
                        }
                      },
                      "performanceConfig": {
                        "latency": "standard"
                      }
                    }
                  }
                }
              }
              # Bedrock sessionId は存在するときのみ渡す (継続会話時のみ)
              if bedrock_session_id:
                request["sessionId"] = bedrock_session_id
              # Bedrock Knowledge Bases への問い合わせ
              response = bedrock_agent.retrieve_and_generate_stream(**request)
              # Bedrock sessionId がレスポンスにあれば、AppSync に送る
              if "sessionId" in response:
                publish_appsync_message(
                  sub,
                  appsync_session_id,
                  {
                    "type": "bedrock_session",
                    "bedrock_session_id": response["sessionId"]
                  },
                  credentials
                )
              for chunk in response["stream"]:
                payload = None
                # Generated text: チャンク分けされた回答メッセージが入る
                if "output" in chunk and "text" in chunk["output"]:
                  payload = {
                    "type": "text",
                    "message": chunk["output"]["text"]
                  }
                  print({"t": chunk["output"]["text"]})
                # Citation: 参考ドキュメントの情報が入る
                elif "citation" in chunk:
                  payload = {
                    "type": "citation",
                    "citation": chunk['citation']['retrievedReferences']
                  }
                  print({"c": chunk['citation']['retrievedReferences']})
                # Continue
                if not payload:
                  continue
                # Publish AppSync
                publish_appsync_message(sub, appsync_session_id, payload, credentials)
            except Exception as e:
              print(str(e))
              raise
    DependsOn:
      - LambdaBedrockKbRole
      - BedrockKnowledgeBase

  LambdaRagSREventInvokeConfig:
    Type: AWS::Lambda::EventInvokeConfig
    Properties:
      FunctionName: !GetAtt LambdaRagSR.Arn
      Qualifier: $LATEST
      MaximumRetryAttempts: 0
      MaximumEventAgeInSeconds: 300

# ------------------------------------------------------------#
# Lambda Bedrock Knowledge Bases Invocation Role (IAM)
# ------------------------------------------------------------#
  LambdaBedrockKbRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub LambdaBedrockKbRole-${SystemName}-${SubName}
      Description: This role allows Lambda functions to invoke Bedrock Knowledge Bases and AppSync Events API.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
      Policies:
        - PolicyName: !Sub LambdaBedrockKbPolicy-${SystemName}-${SubName}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "bedrock:InvokeModel"
                  - "bedrock:InvokeModelWithResponseStream"
                  - "bedrock:GetInferenceProfile"
                  - "bedrock:ListInferenceProfiles"
                Resource:
                  - !Sub "arn:aws:bedrock:*::foundation-model/*"
                  - !Sub "arn:aws:bedrock:*:${AWS::AccountId}:inference-profile/*"
              - Effect: Allow
                Action:
                  - "bedrock:RetrieveAndGenerate"
                  - "bedrock:Retrieve"
                Resource:
                  - !GetAtt BedrockKnowledgeBase.KnowledgeBaseArn
              - Effect: Allow
                Action:
                  - "appsync:connect"
                Resource:
                  - Fn::ImportValue:
                      !Sub AppSyncApiArn-${SystemName}-${SubName}
              - Effect: Allow
                Action:
                  - "appsync:publish"
                  - "appsync:EventPublish"
                Resource:
                  - Fn::Join:
                    - ""
                    - - Fn::ImportValue:
                          !Sub AppSyncApiArn-${SystemName}-${SubName}
                      - /channelNamespace/rag-stream-response

# ------------------------------------------------------------#
# IAM Role for Bedrock Knowledge Base
# ------------------------------------------------------------#
  IAMRoleBedrockKb:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub BedrockKbRole-${SystemName}-${SubName}
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - sts:AssumeRole
            Principal:
              Service:
                - bedrock.amazonaws.com
            Condition:
              StringEquals:
                "aws:SourceAccount": !Sub ${AWS::AccountId}
              ArnLike:
                "aws:SourceArn": !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:knowledge-base/*"
      Policies:
        - PolicyName: !Sub BedrockKbPolicy-${SystemName}-${SubName}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "s3:GetObject"
                  - "s3:ListBucket"
                Resource:
                  - !GetAtt S3BucketKbDatasource.Arn
                  - !Sub ${S3BucketKbDatasource.Arn}/*
                Condition:
                  StringEquals:
                    "aws:ResourceAccount":
                      - !Ref AWS::AccountId
              - Effect: Allow
                Action:
                  - "s3vectors:GetIndex"
                  - "s3vectors:QueryVectors"
                  - "s3vectors:PutVectors"
                  - "s3vectors:GetVectors"
                  - "s3vectors:DeleteVectors"
                Resource:
                  - !GetAtt S3VectorBucketIndex.IndexArn
                Condition:
                  StringEquals:
                    "aws:ResourceAccount":
                      - !Ref AWS::AccountId
              - Effect: Allow
                Action:
                  - "bedrock:InvokeModel"
                Resource:
                  - !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/${EmbeddingModelId}
    DependsOn:
      - S3BucketKbDatasource
      - S3VectorBucketIndex

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
# S3
  S3BucketKbDatasourceName:
    Value: !Ref S3BucketKbDatasource
# API Gateway
  APIGatewayEndpointRagSR:
    Value: !Sub https://${RestApiRagSR}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${RestApiStageRagSR}/ragsr
    Export:
      Name: !Sub RestApiEndpointRagSR-${SystemName}-${SubName}

 

今回、API Gateway を環境に追加しているので、以下の通り AWS Amplify の CloudFormation テンプレートも変更が入っています。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates an Amplify environment.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SystemName:
    Type: String
    Description: System name. use lower case only. (e.g. example)
    Default: example
    MaxLength: 10
    MinLength: 1

  SubName:
    Type: String
    Description: System sub name. use lower case only. (e.g. prod or dev)
    Default: dev
    MaxLength: 10
    MinLength: 1

  DomainName:
    Type: String
    Description: Domain name for URL. xxxxx.xxx (e.g. example.com)
    Default: example.com
    MaxLength: 40
    MinLength: 5

  SubDomainName:
    Type: String
    Description: Sub domain name for URL. (e.g. example-prod or example-dev)
    Default: example-dev
    MaxLength: 20
    MinLength: 1

  NodejsVersion:
    Type: String
    Description: The Node.js version for build phase. (e.g. v22.21.1 as of 2025-12-24)
    Default: v22.21.1
    MaxLength: 10
    MinLength: 6

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Configuration"
        Parameters:
          - SystemName
          - SubName
      - Label:
          default: "Domain Configuration"
        Parameters:
          - DomainName
          - SubDomainName
      - Label:
          default: "Application Configuration"
        Parameters:
          - NodejsVersion

Resources:
# ------------------------------------------------------------#
# Amplify Role (IAM)
# ------------------------------------------------------------#
  AmplifyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub AmplifyExecutionRole-${SystemName}-${SubName}
      Description: This role allows Amplify to pull source codes from CodeCommit.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - amplify.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub AmplifyExecutionPolicy-${SystemName}-${SubName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/amplify/*"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
              - Effect: Allow
                Resource:
                  Fn::ImportValue:
                    !Sub CodeCommitRepoArn-${SystemName}-${SubName}
                Action:
                  - "codecommit:GitPull"

# ------------------------------------------------------------#
#  Amplify Console
# ------------------------------------------------------------#
  AmplifyConsole:
    Type: AWS::Amplify::App
    Properties:
      Name: !Sub ${SystemName}-${SubName}
      Description: !Sub Web App environment for ${SystemName}-${SubName}
      Repository:
        Fn::ImportValue:
          !Sub CodeCommitRepoUrl-${SystemName}-${SubName}
      AutoBranchCreationConfig:
        EnableAutoBranchCreation: false
        EnableAutoBuild: true
        EnablePerformanceMode: false
      EnableBranchAutoDeletion: false
      Platform: WEB
      BuildSpec: |-
        version: 1
        frontend:
          phases:
            preBuild:
              commands:
                - npm ci
            build:
              commands:
                - npm run build
                - echo "VITE_BASE=$VITE_BASE" >> .env
                - echo "VITE_REGION=$VITE_REGION" >> .env
                - echo "VITE_USERPOOLID=$VITE_USERPOOLID" >> .env
                - echo "VITE_USERPOOLWEBCLIENTID=$VITE_USERPOOLWEBCLIENTID" >> .env
                - echo "VITE_IDPOOLID=$VITE_IDPOOLID" >> .env
                - echo "VITE_RESTAPIENDPOINTBEDROCKSR=$VITE_RESTAPIENDPOINTBEDROCKSR" >> .env
                - echo "VITE_RESTAPIENDPOINTRAGSR=$VITE_RESTAPIENDPOINTRAGSR" >> .env
                - echo "VITE_APPSYNCEVENTSHTTPENDPOINT=$VITE_APPSYNCEVENTSHTTPENDPOINT" >> .env
                - echo "VITE_IMG_URL=$VITE_IMG_URL" >> .env
                - echo "VITE_SUBNAME=$VITE_SUBNAME" >> .env
          artifacts:
            baseDirectory: /dist
            files:
              - '**/*'
          cache:
            paths:
              - node_modules/**/*
      CustomHeaders: !Sub |-
        customHeaders:
          - pattern: '**'
            headers:
              - key: 'Strict-Transport-Security'
                value: 'max-age=31536000; includeSubDomains'
              - key: 'X-Frame-Options'
                value: 'DENY'
              - key: 'X-XSS-Protection'
                value: '1; mode=block'
              - key: 'X-Content-Type-Options'
                value: 'nosniff'
              - key: 'Content-Security-Policy'
                value: >-
                  default-src 'self' *.${DomainName} ${DomainName};
                  img-src 'self' *.${DomainName} ${DomainName} data: blob:;
                  style-src 'self' *.${DomainName} ${DomainName} 'unsafe-inline' fonts.googleapis.com;
                  font-src 'self' *.${DomainName} ${DomainName} fonts.gstatic.com;
                  script-src 'self' *.${DomainName} ${DomainName} cdn.jsdelivr.net;
                  script-src-elem 'self' *.${DomainName} ${DomainName} cdn.jsdelivr.net;
                  connect-src 'self' *.${AWS::Region}.amazonaws.com *.${DomainName} ${DomainName} wss:;
                  media-src 'self' *.${DomainName} ${DomainName} data: blob:;
                  worker-src 'self' *.${DomainName} ${DomainName} data: blob:;
              - key: 'Cache-Control'
                value: 'no-store'
      CustomRules:
        - Source: /<*>
          Status: 404-200
          Target: /index.html
        - Source: 
          Status: 200
          Target: /index.html
      EnvironmentVariables:
        - Name: VITE_BASE
          Value: /
        - Name: VITE_REGION
          Value: !Ref AWS::Region
        - Name: VITE_USERPOOLID
          Value:
            Fn::ImportValue:
              !Sub CognitoUserPoolId-${SystemName}-${SubName}
        - Name: VITE_USERPOOLWEBCLIENTID
          Value:
            Fn::ImportValue:
              !Sub CognitoAppClientId-${SystemName}-${SubName}
        - Name: VITE_IDPOOLID
          Value:
            Fn::ImportValue:
              !Sub CognitoIdPoolId-${SystemName}-${SubName}
        - Name: VITE_RESTAPIENDPOINTBEDROCKSR
          Value:
            Fn::ImportValue:
              !Sub RestApiEndpointBedrockSR-${SystemName}-${SubName}
        - Name: VITE_RESTAPIENDPOINTRAGSR
          Value:
            Fn::ImportValue:
              !Sub RestApiEndpointRagSR-${SystemName}-${SubName}
        - Name: VITE_APPSYNCEVENTSHTTPENDPOINT
          Value:
            Fn::ImportValue:
              !Sub AppSyncEventsEndpointHttp-${SystemName}-${SubName}
        - Name: VITE_IMG_URL
          Value: !Sub https://${SubDomainName}-img.${DomainName}
        - Name: VITE_SUBNAME
          Value: !Ref SubName
        - Name: _LIVE_UPDATES
          Value: !Sub '[{"name":"Node.js version","pkg":"node","type":"nvm","version":"${NodejsVersion}"}]'
      IAMServiceRole: !GetAtt AmplifyRole.Arn
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
  AmplifyBranchProd:
    Type: AWS::Amplify::Branch
    Properties:
      AppId: !GetAtt AmplifyConsole.AppId
      BranchName: main
      Description: production
      EnableAutoBuild: true
      EnablePerformanceMode: false
  AmplifyDomainProd:
    Type: AWS::Amplify::Domain
    Properties:
      AppId: !GetAtt AmplifyConsole.AppId
      DomainName: !Ref DomainName
      CertificateSettings:
        CertificateType: CUSTOM
        CustomCertificateArn:
          Fn::ImportValue:
            !Sub AcmCertificateArn-${SystemName}-${SubName}
      SubDomainSettings:
        - BranchName: !GetAtt AmplifyBranchProd.BranchName
          Prefix: !Sub ${SystemName}-${SubName}
      EnableAutoSubDomain: false

 

続編記事

UI 編で、React による UI の開発例を紹介します。

 

まとめ

いかがでしたでしょうか?

Amazon Bedrock Knowledge Bases で RAG チャットボットを開発するときのアーキテクチャを AWS CloudFormation でデプロイする一例を紹介しました。

本記事が皆様のお役に立てれば幸いです。

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