GenUのバックエンド (CDK) 詳細解説　⑥RagKnowledgeBaseStack スタックの解説

## はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、[AWS GenU](https://aws-samples.github.io/generative-ai-use-cases-jp/) およびそれに必要なデータ基盤の探求 ([Snowflake](https://www.snowflake.com/ja/), [dbt](https://www.getdbt.com/), [Iceberg](https://iceberg.apache.org/), etc) に取り組む必要があると考えています。

本投稿では、[GenU のバックエンドである CDK コード](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main/packages/cdk)を詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。

## 前回までのおさらい

前回までで、以下が完了しました。

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)

GenU の CDK は最大で以下の 6 つの子スタックを作成します。

- `CloudFrontWafStack`
- `RagKnowledgeBaseStack`
- `AgentStack`
- `GuardrailStack`
- `GenerativeAiUseCasesStack` ※メインスタック
- `DashboardStack`

今回は GenU 内の `RagKnowledgeBaseStack` スタックを解説していきたいと思います。

### RagKnowledgeBaseStack スタック

RagKnowledgeBaseStack は Bedrock ナレッジベースおよび OpenSearch Serverless
ベクトルストア、データソースである S3 のスタックです。
アーキテクチャ図でいうと、以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/54b34bbc-e0d3-484d-a51d-f490410d12e3.png)

RagKnowledgeBaseStack の実体は [`packages/cdk/lib/rag-knowledge-base-stack.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/rag-knowledge-base-stack.ts) にあります。結構ボリュームが多いですが読んでいきましょう。

```typescript:packages/cdk/lib/rag-knowledge-base-stack.ts
import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as bedrock from 'aws-cdk-lib/aws-bedrock';
import * as oss from 'aws-cdk-lib/aws-opensearchserverless';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3Deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as iam from 'aws-cdk-lib/aws-iam';
import { ProcessedStackInput } from './stack-input';

const UUID = '339C5FED-A1B5-43B6-B40A-5E8E59E5734D';

// 以下が現状 Embedding model としてサポートされているモデル ID
// Dimension は最終的に Custom resource の props として渡すが
// 勝手に型が変換されてしまう Issue があるため、number ではなく string にしておく
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1037
const MODEL_VECTOR_MAPPING: { [key: string]: string } = {
  'amazon.titan-embed-text-v1': '1536',
  'amazon.titan-embed-text-v2:0': '1024',
  'cohere.embed-multilingual-v3': '1024',
  'cohere.embed-english-v3': '1024',
};

// parsingConfiguration で PDF ファイルの中に埋め込まれている画像やグラフや表を読み取る機能がある。
// 読み取る際のプロンプトは任意のものが定義できる。以下に const として定義する。利用環境によってプロンプトを変更することで、より高い精度を期待できる。
// https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html#kb-advanced-parsing
const PARSING_PROMPT = `ドキュメントに含まれる画像やグラフや表などの Image コンテンツからテキストを書き写して、コードブロックではないMarkdown構文で出力してください。以下の手順に従ってください：

1. 提供されたページを注意深く調べてください。

2. ページに存在するすべての要素を特定してください。これには見出し、本文、脚注、表、視覚化、キャプション、ページ番号などが含まれます。

3. Markdown構文のフォーマットを使用して出力してください :
- 見出し：主見出しには#、セクションには##、サブセクションには###など
- リスト：箇条書きには* または -、番号付きリストには1. 2. 3.
- 繰り返しは避けてください

4. 要素が Visualization の場合：
- 自然言語で詳細な説明を提供してください
- 説明を提供した後、Visualization 内のテキストは転写しないでください

5. 要素が表の場合：
- Markdownの表を作成し、すべての行が同じ列数を持つようにしてください
- セルの配置をできるだけ忠実に維持してください
- 表を複数の表に分割しないでください
- 結合されたセルが複数の行や列にまたがる場合、テキストを左上のセルに配置し、他のセルには ' ' を出力してください
- 列の区切りには | を使用し、ヘッダー行の区切りには |-|-| を使用してください
- セルに複数の項目がある場合、別々の行にリストしてください
- 表にサブヘッダーがある場合、サブヘッダーをヘッダーから別の行で分離してください

6. 要素が段落の場合：
- 各テキスト要素を表示されているとおりに正確に転写してください

7. 要素がヘッダー、フッター、脚注、ページ番号の場合：
- 各テキスト要素を表示されているとおりに正確に転写してください

出力例：

Y軸に「売上高（$百万）」、X軸に「年」とラベル付けされた年間売上高を示す棒グラフ。グラフには2018年（$12M）、2019年（$18M）、2020年（$8M）、2021年（$22M）の棒がある。
図3：このグラフは年間売上高を百万ドル単位で示しています。2020年はCOVID-19パンデミックの影響で大幅に減少しました。

年次報告書
財務ハイライト
収益：$40M
利益：$12M
EPS：$1.25
| | 12月31日終了年度 | |

2021	2022
キャッシュフロー：
営業活動	$ 46,327	$ 46,752
投資活動	(58,154)	(37,601)
財務活動	6,291	9,718`;

const EMBEDDING_MODELS = Object.keys(MODEL_VECTOR_MAPPING);

interface OpenSearchServerlessIndexProps {
  collectionId: string;
  vectorIndexName: string;
  vectorField: string;
  metadataField: string;
  textField: string;
  vectorDimension: string;
}

class OpenSearchServerlessIndex extends Construct {
  public readonly customResourceHandler: lambda.IFunction;
  public readonly customResource: cdk.CustomResource;

  constructor(
    scope: Construct,
    id: string,
    props: OpenSearchServerlessIndexProps
  ) {
    super(scope, id);

    const customResourceHandler = new lambda.SingletonFunction(
      this,
      'OpenSearchServerlessIndex',
      {
        runtime: lambda.Runtime.NODEJS_LATEST,
        code: lambda.Code.fromAsset('custom-resources'),
        handler: 'oss-index.handler',
        uuid: UUID,
        lambdaPurpose: 'OpenSearchServerlessIndex',
        timeout: cdk.Duration.minutes(15),
      }
    );

    const customResource = new cdk.CustomResource(this, 'CustomResource', {
      serviceToken: customResourceHandler.functionArn,
      resourceType: 'Custom::OssIndex',
      properties: props,
    });

    this.customResourceHandler = customResourceHandler;
    this.customResource = customResource;
  }
}

export interface RagKnowledgeBaseStackProps extends StackProps {
  params: ProcessedStackInput;
  collectionName?: string;
  vectorIndexName?: string;
  vectorField?: string;
  metadataField?: string;
  textField?: string;
}

export class RagKnowledgeBaseStack extends Stack {
  public readonly knowledgeBaseId: string;
  public readonly dataSourceBucketName: string;

  constructor(scope: Construct, id: string, props: RagKnowledgeBaseStackProps) {
    super(scope, id, props);

    const {
      env,
      embeddingModelId,
      ragKnowledgeBaseStandbyReplicas,
      ragKnowledgeBaseAdvancedParsing,
      ragKnowledgeBaseAdvancedParsingModelId,
    } = props.params;

    if (typeof embeddingModelId !== 'string') {
      throw new Error(
        'Knowledge Base RAG が有効になっていますが、embeddingModelId が指定されていません'
      );
    }

    if (!EMBEDDING_MODELS.includes(embeddingModelId)) {
      throw new Error(
        `embeddingModelId が無効な値です (有効な embeddingModelId ${EMBEDDING_MODELS})`
      );
    }

    const collectionName =
      props.collectionName ?? `generative-ai-use-cases-jp${env.toLowerCase()}`;
    const vectorIndexName =
      props.vectorIndexName ?? 'bedrock-knowledge-base-default';
    const vectorField =
      props.vectorField ?? 'bedrock-knowledge-base-default-vector';
    const textField = props.textField ?? 'AMAZON_BEDROCK_TEXT_CHUNK';
    const metadataField = props.metadataField ?? 'AMAZON_BEDROCK_METADATA';

    const knowledgeBaseRole = new iam.Role(this, 'KnowledgeBaseRole', {
      assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
    });

    if (
      ragKnowledgeBaseAdvancedParsing &&
      typeof ragKnowledgeBaseAdvancedParsingModelId !== 'string'
    ) {
      throw new Error(
        'Knowledge Base RAG の Advanced Parsing が有効ですが、ragKnowledgeBaseAdvancedParsingModelId が指定されていないか、文字列ではありません'
      );
    }

    const collection = new oss.CfnCollection(this, 'Collection', {
      name: collectionName,
      description: 'GenU Collection',
      type: 'VECTORSEARCH',
      standbyReplicas: ragKnowledgeBaseStandbyReplicas ? 'ENABLED' : 'DISABLED',
    });

    const ossIndex = new OpenSearchServerlessIndex(this, 'OssIndex', {
      collectionId: collection.ref,
      vectorIndexName,
      vectorField,
      textField,
      metadataField,
      vectorDimension: MODEL_VECTOR_MAPPING[embeddingModelId],
    });

    ossIndex.customResourceHandler.addToRolePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [cdk.Token.asString(collection.getAtt('Arn'))],
        actions: ['aoss:APIAccessAll'],
      })
    );

    const accessPolicy = new oss.CfnAccessPolicy(this, 'AccessPolicy', {
      name: collectionName,
      policy: JSON.stringify([
        {
          Rules: [
            {
              Resource: [`collection/${collectionName}`],
              Permission: [
                'aoss:DescribeCollectionItems',
                'aoss:CreateCollectionItems',
                'aoss:UpdateCollectionItems',
              ],
              ResourceType: 'collection',
            },
            {
              Resource: [`index/${collectionName}/*`],
              Permission: [
                'aoss:UpdateIndex',
                'aoss:DescribeIndex',
                'aoss:ReadDocument',
                'aoss:WriteDocument',
                'aoss:CreateIndex',
                'aoss:DeleteIndex',
              ],
              ResourceType: 'index',
            },
          ],
          Principal: [
            knowledgeBaseRole.roleArn,
            ossIndex.customResourceHandler.role?.roleArn,
          ],
          Description: '',
        },
      ]),
      type: 'data',
    });

    const networkPolicy = new oss.CfnSecurityPolicy(this, 'NetworkPolicy', {
      name: collectionName,
      policy: JSON.stringify([
        {
          Rules: [
            {
              Resource: [`collection/${collectionName}`],
              ResourceType: 'collection',
            },
            {
              Resource: [`collection/${collectionName}`],
              ResourceType: 'dashboard',
            },
          ],
          AllowFromPublic: true,
        },
      ]),
      type: 'network',
    });

    const encryptionPolicy = new oss.CfnSecurityPolicy(
      this,
      'EncryptionPolicy',
      {
        name: collectionName,
        policy: JSON.stringify({
          Rules: [
            {
              Resource: [`collection/${collectionName}`],
              ResourceType: 'collection',
            },
          ],
          AWSOwnedKey: true,
        }),
        type: 'encryption',
      }
    );

    collection.node.addDependency(accessPolicy);
    collection.node.addDependency(networkPolicy);
    collection.node.addDependency(encryptionPolicy);

    const accessLogsBucket = new s3.Bucket(this, 'DataSourceAccessLogsBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      autoDeleteObjects: true,
      removalPolicy: RemovalPolicy.DESTROY,
      objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
      enforceSSL: true,
    });

    const dataSourceBucket = new s3.Bucket(this, 'DataSourceBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      autoDeleteObjects: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
      serverAccessLogsBucket: accessLogsBucket,
      serverAccessLogsPrefix: 'AccessLogs/',
      enforceSSL: true,
    });

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: ['*'],
        actions: ['bedrock:InvokeModel'],
      })
    );

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [cdk.Token.asString(collection.getAtt('Arn'))],
        actions: ['aoss:APIAccessAll'],
      })
    );

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}`],
        actions: ['s3:ListBucket'],
      })
    );

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}/*`],
        actions: ['s3:GetObject'],
      })
    );

    const knowledgeBase = new bedrock.CfnKnowledgeBase(this, 'KnowledgeBase', {
      name: collectionName,
      roleArn: knowledgeBaseRole.roleArn,
      knowledgeBaseConfiguration: {
        type: 'VECTOR',
        vectorKnowledgeBaseConfiguration: {
          embeddingModelArn: `arn:aws:bedrock:${this.region}::foundation-model/${embeddingModelId}`,
        },
      },
      storageConfiguration: {
        type: 'OPENSEARCH_SERVERLESS',
        opensearchServerlessConfiguration: {
          collectionArn: cdk.Token.asString(collection.getAtt('Arn')),
          fieldMapping: {
            metadataField,
            textField,
            vectorField,
          },
          vectorIndexName,
        },
      },
    });

    new bedrock.CfnDataSource(this, 'DataSource', {
      dataSourceConfiguration: {
        s3Configuration: {
          bucketArn: `arn:aws:s3:::${dataSourceBucket.bucketName}`,
          inclusionPrefixes: ['docs/'],
        },
        type: 'S3',
      },
      vectorIngestionConfiguration: {
        ...(ragKnowledgeBaseAdvancedParsing
          ? {
              // Advanced Parsing を有効化する場合のみ、parsingConfiguration を構成する
              parsingConfiguration: {
                parsingStrategy: 'BEDROCK_FOUNDATION_MODEL',
                bedrockFoundationModelConfiguration: {
                  modelArn: `arn:aws:bedrock:${this.region}::foundation-model/${ragKnowledgeBaseAdvancedParsingModelId}`,
                  parsingPrompt: {
                    parsingPromptText: PARSING_PROMPT,
                  },
                },
              },
            }
          : {}),
        // チャンク戦略を変更したい場合は、以下のコメントアウトを外して、各種パラメータを調整することで、環境に合わせた環境構築が可能です。
        // 以下の 4 種類のチャンク戦略が選択可能です。
        // - デフォルト (何も指定しない)
        // - セマンティックチャンク
        // - 階層チャンク
        // - 標準チャンク
        // 詳細は以下の Document を参照ください。
        // https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html
        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrock.CfnDataSource.ChunkingConfigurationProperty.html
        //
        // セマンティックチャンク
        // chunkingConfiguration: {
        //   chunkingStrategy: 'SEMANTIC',
        //   semanticChunkingConfiguration: {
        //     maxTokens: 300,
        //     bufferSize: 0,
        //     breakpointPercentileThreshold: 95,
        //   },
        // },
        //
        // 階層チャンク
        // chunkingConfiguration: {
        //   chunkingStrategy: 'HIERARCHICAL',
        //   hierarchicalChunkingConfiguration: {
        //     levelConfigurations: [
        //       {
        //         maxTokens: 1500, // 親チャンクの Max Token サイズ
        //       },
        //       {
        //         maxTokens: 300, // 子チャンクの Max Token サイズ
        //       },
        //     ],
        //     overlapTokens: 60,
        //   },
        // },
        //
        // 標準チャンク
        // chunkingConfiguration: {
        //   chunkingStrategy: 'FIXED_SIZE',
        //   fixedSizeChunkingConfiguration: {
        //     maxTokens: 300,
        //     overlapPercentage: 10,
        //   },
        // },
      },
      knowledgeBaseId: knowledgeBase.ref,
      name: 's3-data-source',
    });

    knowledgeBase.addDependency(collection);
    knowledgeBase.node.addDependency(ossIndex.customResource);

    new s3Deploy.BucketDeployment(this, 'DeployDocs', {
      sources: [s3Deploy.Source.asset('./rag-docs')],
      destinationBucket: dataSourceBucket,
      // 以前の設定で同 Bucket にアクセスログが残っている可能性があるため、この設定は残す
      exclude: ['AccessLogs/*', 'logs*'],
    });

    this.knowledgeBaseId = knowledgeBase.ref;
    this.dataSourceBucketName = dataSourceBucket.bucketName;
  }
}
```

このスタックでは、以下のリソース群を作成しています。

- `Role`
- `CfnCollection`
- `OpenSearchServerlessIndex`
- `CfnAccessPolicy`
- `CfnSecurityPolicy` 2 つ
- `Bucket` 2 つ
- `CfnKnowledgeBase`
- `CfnDataSource`
- `BucketDeployment`

上から 1 つずつ見ていきます。

#### RagKnowledgeBaseStack > Role リソース

Role は Bedrock ナレッジベースにアタッチするロールです。
以下のソースコードが Role の定義です。

```typescript:packages/cdk/lib/rag-knowledge-base-stack.ts (抜粋)
    const knowledgeBaseRole = new iam.Role(this, 'KnowledgeBaseRole', {
      assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
    });

    /* 中略 */

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: ['*'],
        actions: ['bedrock:InvokeModel'],
      })
    );

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [cdk.Token.asString(collection.getAtt('Arn'))],
        actions: ['aoss:APIAccessAll'],
      })
    );

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}`],
        actions: ['s3:ListBucket'],
      })
    );

    knowledgeBaseRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}/*`],
        actions: ['s3:GetObject'],
      })
    );
```

この中では、以下の AWS リソースを生成しています。

- `KnowledgeBaseRole` ロール
  - `bedrock:InvokeModel` の許可権限
    - 対象リソースは全リソース
  - `aoss:APIAccessAll` の許可権限
    - 対象リソースは OpenSearchServerless コレクション(後述)
  - `s3:ListBucket` の許可権限
    - 対象リソースは S3 の dataSourceBucket バケット(後述)
  - `s3:GetObject` の許可権限
    - 対象リソースは S3 の dataSourceBucket バケット(後述)

このロールを後述する `CfnKnowledgeBase` にアタッチすることで、ナレッジベースから Bedrock のモデル呼び出し (おそらく後述のプロンプトによるパース用)、OpenSearchServerless の API 呼び出し、S3 からのファイル取得が行えるようになります。

#### RagKnowledgeBaseStack > CfnCollection リソース

CfnCollection は [OpenSearchServerless コレクション](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-manage.html)リソースです。
以下のソースコードが CfnCollection の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    const {
      env,
      embeddingModelId,
      ragKnowledgeBaseStandbyReplicas,
      ragKnowledgeBaseAdvancedParsing,
      ragKnowledgeBaseAdvancedParsingModelId,
    } = props.params;

    /* 中略 */

    const collectionName =
      props.collectionName ?? `generative-ai-use-cases-jp${env.toLowerCase()}`;

    /* 中略 */

    const collection = new oss.CfnCollection(this, 'Collection', {
      name: collectionName,
      description: 'GenU Collection',
      type: 'VECTORSEARCH',
      standbyReplicas: ragKnowledgeBaseStandbyReplicas ? 'ENABLED' : 'DISABLED',
    });
```

この中では、以下の AWS リソースを生成しています。

- `generative-ai-use-cases-jp` OpenSearchServerless コレクション
  - コレクションタイプは「ベクトル検索 (VECTORSEARCH) 」
  - レプリカはパラメータ `ragKnowledgeBaseStandbyReplicas` で有効/無効を設定
    - 初期値は false [`packages/cdk/lib/stack-input.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/stack-input.ts) で設定
    - 有効化すると可用性が上がるが、コストも上がる

この OpenSearchServerless コレクションに対し、後述する分析用のインデックスを追加したり、アクセスポリシーやネットワークポリシーを追加していきます。

#### RagKnowledgeBaseStack > OpenSearchServerlessIndex リソース

OpenSearchServerlessIndex は OpenSearch Serverless のインデックスリソース (を作成するカスタムリソース) です。
以下のソースコードが OpenSearchServerlessIndex の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
// 以下が現状 Embedding model としてサポートされているモデル ID
// Dimension は最終的に Custom resource の props として渡すが
// 勝手に型が変換されてしまう Issue があるため、number ではなく string にしておく
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1037
const MODEL_VECTOR_MAPPING: { [key: string]: string } = {
  'amazon.titan-embed-text-v1': '1536',
  'amazon.titan-embed-text-v2:0': '1024',
  'cohere.embed-multilingual-v3': '1024',
  'cohere.embed-english-v3': '1024',
};

/* 中略 */

class OpenSearchServerlessIndex extends Construct {
  public readonly customResourceHandler: lambda.IFunction;
  public readonly customResource: cdk.CustomResource;

  constructor(
    scope: Construct,
    id: string,
    props: OpenSearchServerlessIndexProps
  ) {
    super(scope, id);

    const customResourceHandler = new lambda.SingletonFunction(
      this,
      'OpenSearchServerlessIndex',
      {
        runtime: lambda.Runtime.NODEJS_LATEST,
        code: lambda.Code.fromAsset('custom-resources'),
        handler: 'oss-index.handler',
        uuid: UUID,
        lambdaPurpose: 'OpenSearchServerlessIndex',
        timeout: cdk.Duration.minutes(15),
      }
    );

    const customResource = new cdk.CustomResource(this, 'CustomResource', {
      serviceToken: customResourceHandler.functionArn,
      resourceType: 'Custom::OssIndex',
      properties: props,
    });

    this.customResourceHandler = customResourceHandler;
    this.customResource = customResource;
  }
}

    /* 中略 */

    const {
      env,
      embeddingModelId,
      ragKnowledgeBaseStandbyReplicas,
      ragKnowledgeBaseAdvancedParsing,
      ragKnowledgeBaseAdvancedParsingModelId,
    } = props.params;

    if (typeof embeddingModelId !== 'string') {
      throw new Error(
        'Knowledge Base RAG が有効になっていますが、embeddingModelId が指定されていません'
      );
    }

    if (!EMBEDDING_MODELS.includes(embeddingModelId)) {
      throw new Error(
        `embeddingModelId が無効な値です (有効な embeddingModelId ${EMBEDDING_MODELS})`
      );
    }

    /* 中略 */

    const vectorIndexName =
      props.vectorIndexName ?? 'bedrock-knowledge-base-default';
    const vectorField =
      props.vectorField ?? 'bedrock-knowledge-base-default-vector';
    const textField = props.textField ?? 'AMAZON_BEDROCK_TEXT_CHUNK';
    const metadataField = props.metadataField ?? 'AMAZON_BEDROCK_METADATA';

    /* 中略 */

    const ossIndex = new OpenSearchServerlessIndex(this, 'OssIndex', {
      collectionId: collection.ref,
      vectorIndexName,
      vectorField,
      textField,
      metadataField,
      vecotrDimension: MODEL_VECTOR_MAPPING[embeddingModelId],
    });

    ossIndex.customResourceHandler.addToRolePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [cdk.Token.asString(collection.getAtt('Arn'))],
        actions: ['aoss:APIAccessAll'],
      })
    );
```

この中では、以下の AWS リソースを生成しています。

- `OpenSearchServerlessIndex` [コンストラクト](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/constructs.html)
  - [`SingletonFunction`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.SingletonFunction.html) lambda 関数
    - コンストラクトとして部品化しているため、CDK 内で 1 つの Lambda 関数となるよう`SingletonFunction` で定義されている
    - [ソースコードは後述](https://qiita.com/drafts/1223c9d22e73168a8809/edit#singletonfunction-%E9%96%A2%E6%95%B0%E3%81%AE%E5%87%A6%E7%90%86)
    - `addToRolePolicy` にて OpenSearchServerless コレクションの API 呼び出しを許可
  - [`CDKのカスタムリソース`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CustomResource.html)
    - 上記の Lambda 関数を呼び出す
    - 呼び出し時のパラメータは以下の通り
      - collectionId: 前述の OpenSearchServerless コレクションの[コレクション ID](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-opensearchserverless-collection.html#aws-resource-opensearchserverless-collection-return-values)
      - vectorIndexName: パラメータ値。初期値は `bedrock-knowledge-base-default`
      - vectorField: パラメータ値。初期値は `bedrock-knowledge-base-default-vector`
      - textField: パラメータ値。初期値は `AMAZON_BEDROCK_TEXT_CHUNK`
      - metadataField: パラメータ値。初期値は `AMAZON_BEDROCK_METADATA`
      - vecotrDimension: `MODEL_VECTOR_MAPPING[embeddingModelId]` にてベクトル次元数を取得
        - `embeddingModelId` は必須かつ以下のいずれか
          - `amazon.titan-embed-text-v1` => 1536 次元が設定される,
          - `amazon.titan-embed-text-v2:0` => 1024 次元が設定される,
          - `cohere.embed-multilingual-v3` => 1024 次元が設定される,
          - `cohere.embed-english-v3` => 1024 次元が設定される,

##### OpenSearchServerlessIndex 関数の処理

この `OpenSearchServerlessIndex` 関数の実体は [`packages/cdk/custom-resources/oss-index.js`] にあります。

```javascript:packages/cdk/custom-resources/oss-index.js
const { defaultProvider } = require('@aws-sdk/credential-provider-node');
const { Client } = require('@opensearch-project/opensearch');
const { AwsSigv4Signer } = require('@opensearch-project/opensearch/aws');

const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));

const updateStatus = async (event, status, reason, physicalResourceId) => {
  const body = JSON.stringify({
    Status: status,
    Reason: reason,
    PhysicalResourceId: physicalResourceId,
    StackId: event.StackId,
    RequestId: event.RequestId,
    LogicalResourceId: event.LogicalResourceId,
    NoEcho: false,
    Data: {},
  });

  const res = await fetch(event.ResponseURL, {
    method: 'PUT',
    body,
    headers: {
      'Content-Type': '',
      'Content-Length': body.length.toString(),
    },
  });

  // 失敗時の記録のために残す
  console.log(res);
  console.log(await res.text());
};

exports.handler = async (event, context) => {
  // 失敗時の記録のために残す
  console.log(event);

  const props = event.ResourceProperties;
  const collectionId = props.collectionId;
  const region = process.env.AWS_DEFAULT_REGION;
  const client = new Client({
    ...AwsSigv4Signer({
      region,
      service: 'aoss',
      getCredentials: () => {
        const credentialsProvider = defaultProvider();
        return credentialsProvider();
      },
    }),
    node: `https://${collectionId}.${region}.aoss.amazonaws.com`,
  });

  try {
    switch (event.RequestType) {
      case 'Create':
        await client.indices.create({
          index: props.vectorIndexName,
          body: {
            mappings: {
              properties: {
                [props.metadataField]: {
                  type: 'text',
                  index: false,
                },
                [props.textField]: {
                  type: 'text',
                  analyzer: 'custom_kuromoji_analyzer',
                },
                [props.vectorField]: {
                  type: 'knn_vector',
                  dimension: Number(props.vectorDimension),
                  method: {
                    engine: 'faiss',
                    space_type: 'l2',
                    name: 'hnsw',
                    parameters: {},
                  },
                },
              },
            },
            settings: {
              index: {
                knn: true,
                analysis: {
                  analyzer: {
                    custom_kuromoji_analyzer: {
                      type: 'custom',
                      tokenizer: 'kuromoji_tokenizer',
                      filter: [
                        'kuromoji_baseform',
                        'kuromoji_part_of_speech',
                        'kuromoji_stemmer',
                        'lowercase',
                        'ja_stop',
                      ],
                      char_filter: [
                        'kuromoji_iteration_mark',
                        'icu_normalizer',
                        'html_strip',
                      ],
                    },
                  },
                },
              },
            },
          },
        });
        await sleep(60 * 1000); // sleep 60s to confirm the creation
        await updateStatus(
          event,
          'SUCCESS',
          'Successfully created',
          props.vectorIndexName
        );
        break;
      case 'Update':
        await updateStatus(
          event,
          'SUCCESS',
          'Update operation is not supported',
          props.vectorIndexName
        );
        break;
      case 'Delete':
        const index = event.PhysicalResourceId;
        await client.indices.delete({
          index,
        });
        await updateStatus(event, 'SUCCESS', 'Successfully deleted', index);
        break;
    }
  } catch (e) {
    console.log('---- Error');
    console.log(e);

    const physicalResourceId =
      props.vectorIndexName || event.PhysicalResourceId;
    await updateStatus(event, 'FAILED', e.message, physicalResourceId);
  }
};
```

この Lambda 関数では AWS SDK を用いて OpenSearch Serverless 用のインデックスを作成しています。

- Amazon OpenSearch Serverless クライアントの作成
  - パラメータの collectionId を元に [Amazon OpenSearch Serverless クライアント](https://www.npmjs.com/package/@opensearch-project/opensearch)を作成
- スタック操作 `event.RequestType` によって以下の処理を実行
  - リソース作成: `Create`
    - [opensearch-js を利用してインデックスを作成](https://github.com/opensearch-project/opensearch-js/blob/HEAD/USER_GUIDE.md#create-an-index)
      - パラメータの `vectorIndexName`, `metadataField`, `textField`, `vectorField`, `vecotrDimension` を元に mappings を設定
      - settings で日本語のトークナイザーとしてよく使われる`kuromoji_tokenizer`をカスタマイズ指定
        - filter: `kuromoji_baseform`, `kuromoji_part_of_speech`, `kuromoji_stemmer`, `lowercase`, `ja_stop`
        - char_filter: `kuromoji_iteration_mark`, `icu_normalizer`, `html_strip`
    - 60 秒待つ
    - ステータスを `SUCCESS`, メッセージを `Successfully created` に更新
  - リソース更新: `Update`
    - 何もしない
    - ステータスを `SUCCESS`, メッセージを `Update operation is not supported` に更新
  - リソース削除: `Delete`
    - リソースを削除
    - ステータスを `SUCCESS`, メッセージを `Successfully deleted` に更新

これを見ると、`Create` の場合、作成が終わっていても終わっていなくても 60 秒後に`SUCCESS`を返すようです。

#### RagKnowledgeBaseStack > CfnAccessPolicy リソース

CfnAccessPolicy は、OpenSearch Serverless のデータアクセスポリシーです。
以下のソースコードが CfnAccessPolicy の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    const accessPolicy = new oss.CfnAccessPolicy(this, 'AccessPolicy', {
      name: collectionName,
      policy: JSON.stringify([
        {
          Rules: [
            {
              Resource: [`collection/${collectionName}`],
              Permission: [
                'aoss:DescribeCollectionItems',
                'aoss:CreateCollectionItems',
                'aoss:UpdateCollectionItems',
              ],
              ResourceType: 'collection',
            },
            {
              Resource: [`index/${collectionName}/*`],
              Permission: [
                'aoss:UpdateIndex',
                'aoss:DescribeIndex',
                'aoss:ReadDocument',
                'aoss:WriteDocument',
                'aoss:CreateIndex',
                'aoss:DeleteIndex',
              ],
              ResourceType: 'index',
            },
          ],
          Principal: [
            knowledgeBaseRole.roleArn,
            ossIndex.customResourceHandler.role?.roleArn,
          ],
          Description: '',
        },
      ]),
      type: 'data',
    });

    /* 中略 */

    collection.node.addDependency(accessPolicy);
```

この中では、[サポートされているポリシーのアクセス許可](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-data-access.html#serverless-data-supported-permissions)のうち、`DeleteCollectionItems`を除く以下のポリシーが許可されています。

- OpenSearch Serverless コレクションへのアイテム登録/更新/削除
- OpenSearch Serverless インデックスの登録/更新/削除

また、OpenSearch Serverless コレクションはこのアクセスポリシーに依存すること (アクセスポリシー作成後にコレクションを作成すること) が明示されています。

#### RagKnowledgeBaseStack > CfnSecurityPolicy リソース

CfnSecurityPolicy は、OpenSearch Serverless のネットワークポリシー、暗号化ポリシーを設定しています。
以下のソースコードが CfnSecurityPolicy の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    const networkPolicy = new oss.CfnSecurityPolicy(this, 'NetworkPolicy', {
      name: collectionName,
      policy: JSON.stringify([
        {
          Rules: [
            {
              Resource: [`collection/${collectionName}`],
              ResourceType: 'collection',
            },
            {
              Resource: [`collection/${collectionName}`],
              ResourceType: 'dashboard',
            },
          ],
          AllowFromPublic: true,
        },
      ]),
      type: 'network',
    });

    const encryptionPolicy = new oss.CfnSecurityPolicy(
      this,
      'EncryptionPolicy',
      {
        name: collectionName,
        policy: JSON.stringify({
          Rules: [
            {
              Resource: [`collection/${collectionName}`],
              ResourceType: 'collection',
            },
          ],
          AWSOwnedKey: true,
        }),
        type: 'encryption',
      }
    );

    /* 中略 */

    collection.node.addDependency(networkPolicy);
    collection.node.addDependency(encryptionPolicy);
```

この中では、OpenSearch Serverless の[ネットワークポリシー](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-network.html)と[暗号化ポリシー](https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/serverless-encryption.html)を生成しています。

- ネットワークポリシー
  - `dashboard`, `collection` 共にパブリックアクセスを許可
- 暗号化ポリシー
  - AWS マネージドキーを使用して `collection` を暗号化

また、アクセスポリシー同様、OpenSearch Serverless コレクションはこれらのポリシーに依存することが明示されています。

#### RagKnowledgeBaseStack > Bucket リソース

Bucket はデータソースおよびアクセスログ保管用の S3 バケットリソースです。
以下のソースコードが Bucket の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    const accessLogsBucket = new s3.Bucket(this, 'DataSourceAccessLogsBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      autoDeleteObjects: true,
      removalPolicy: RemovalPolicy.DESTROY,
      objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
      enforceSSL: true,
    });

    const dataSourceBucket = new s3.Bucket(this, 'DataSourceBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      autoDeleteObjects: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
      serverAccessLogsBucket: accessLogsBucket,
      serverAccessLogsPrefix: 'AccessLogs/',
      enforceSSL: true,
    });
```

この中では、以下の S3 バケットを生成しています。

- アクセスログ保管用バケット
  - パブリックアクセス: ブロック
  - 暗号化: AWS マネージドキーでの暗号化
  - 削除ポリシー: バケットを削除 (バケットに含まれるオブジェクトも削除)
  - [オブジェクトの所有者](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.ObjectOwnership.html): オブジェクトをアップロードしたアカウント
  - SSL アクセス: 強制
- データソースバケット
  - パブリックアクセス: ブロック
  - 暗号化: AWS マネージドキーでの暗号化
  - 削除ポリシー: バケットを削除 (バケットに含まれるオブジェクトも削除)
  - [オブジェクトの所有者](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.ObjectOwnership.html): オブジェクトをアップロードしたアカウント
  - アクセスログ: アクセスログ保管用バケットの `AccessLogs/` 配下を指定
  - SSL アクセス: 強制

また、前述のロールの指定により、 Bedrock ナレッジベースはデータソース S3 バケットに対し、オブジェクトの取得を許可しています。

#### RagKnowledgeBaseStack > CfnKnowledgeBase リソース

CfnKnowledgeBase は Bedrock ナレッジベースのリソースです。
以下のソースコードが CfnKnowledgeBase の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    const knowledgeBase = new bedrock.CfnKnowledgeBase(this, 'KnowledgeBase', {
      name: collectionName,
      roleArn: knowledgeBaseRole.roleArn,
      knowledgeBaseConfiguration: {
        type: 'VECTOR',
        vectorKnowledgeBaseConfiguration: {
          embeddingModelArn: `arn:aws:bedrock:${this.region}::foundation-model/${embeddingModelId}`,
        },
      },
      storageConfiguration: {
        type: 'OPENSEARCH_SERVERLESS',
        opensearchServerlessConfiguration: {
          collectionArn: cdk.Token.asString(collection.getAtt('Arn')),
          fieldMapping: {
            metadataField,
            textField,
            vectorField,
          },
          vectorIndexName,
        },
      },
    });

    /* 中略 */

    knowledgeBase.addDependency(collection);
    knowledgeBase.node.addDependency(ossIndex.customResource);
```

この中では、以下の Bedrock ナレッジベースを生成しています。

- 名前は OpenSearch Serverless のコレクションと同名
- 前述の Bedrock ナレッジベース用ロールを指定
- 変換するデータのタイプは [ベクトル](https://docs.aws.amazon.com/ja_jp/bedrock/latest/APIReference/API_agent_KnowledgeBaseConfiguration.html) を指定
- 生成 AI のモデルは前述の通りパラメータの `embeddingModelId` で指定する (必須かつ以下のいずれか)
  - `amazon.titan-embed-text-v1`
  - `amazon.titan-embed-text-v2:0`
  - `cohere.embed-multilingual-v3`
  - `cohere.embed-english-v3`
- ストレージに前述の OpenSearch Serverless コレクションを指定

また、Bedrock ナレッジベースは、OpenSearch Serverless コレクション、および、インデックス (を作成する Lambda 関数カスタムリソース) に依存させることが明示されています。

#### RagKnowledgeBaseStack > CfnDataSource リソース

CfnDataSource は Bedrock データソースのリソースです。
Bedrock ナレッジベースがベクトルストアのリソースであることに対し、Bedrock データソースは外部データをナレッジベースに取り込む際の設定を定義します。

以下のソースコードが CfnDataSource の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    new bedrock.CfnDataSource(this, 'DataSource', {
      dataSourceConfiguration: {
        s3Configuration: {
          bucketArn: `arn:aws:s3:::${dataSourceBucket.bucketName}`,
          inclusionPrefixes: ['docs/'],
        },
        type: 'S3',
      },
      vectorIngestionConfiguration: {
        ...(ragKnowledgeBaseAdvancedParsing
          ? {
              // Advanced Parsing を有効化する場合のみ、parsingConfiguration を構成する
              parsingConfiguration: {
                parsingStrategy: 'BEDROCK_FOUNDATION_MODEL',
                bedrockFoundationModelConfiguration: {
                  modelArn: `arn:aws:bedrock:${this.region}::foundation-model/${ragKnowledgeBaseAdvancedParsingModelId}`,
                  parsingPrompt: {
                    parsingPromptText: PARSING_PROMPT,
                  },
                },
              },
            }
          : {}),
        // チャンク戦略を変更したい場合は、以下のコメントアウトを外して、各種パラメータを調整することで、環境に合わせた環境構築が可能です。
        // 以下の 4 種類のチャンク戦略が選択可能です。
        // - デフォルト (何も指定しない)
        // - セマンティックチャンク
        // - 階層チャンク
        // - 標準チャンク
        // 詳細は以下の Document を参照ください。
        // https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html
        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_bedrock.CfnDataSource.ChunkingConfigurationProperty.html
        //
        // セマンティックチャンク
        // chunkingConfiguration: {
        //   chunkingStrategy: 'SEMANTIC',
        //   semanticChunkingConfiguration: {
        //     maxTokens: 300,
        //     bufferSize: 0,
        //     breakpointPercentileThreshold: 95,
        //   },
        // },
        //
        // 階層チャンク
        // chunkingConfiguration: {
        //   chunkingStrategy: 'HIERARCHICAL',
        //   hierarchicalChunkingConfiguration: {
        //     levelConfigurations: [
        //       {
        //         maxTokens: 1500, // 親チャンクの Max Token サイズ
        //       },
        //       {
        //         maxTokens: 300, // 子チャンクの Max Token サイズ
        //       },
        //     ],
        //     overlapTokens: 60,
        //   },
        // },
        //
        // 標準チャンク
        // chunkingConfiguration: {
        //   chunkingStrategy: 'FIXED_SIZE',
        //   fixedSizeChunkingConfiguration: {
        //     maxTokens: 300,
        //     overlapPercentage: 10,
        //   },
        // },
      },
      knowledgeBaseId: knowledgeBase.ref,
      name: 's3-data-source',
    });
```

この中では、以下の Bedrock データソースを生成しています。

- 名前は固定値 `s3-data-source`
- データソースとして前述のデータソース S3 バケットを指定 (パスプレフィックスは `docs/`)
- データソースを取り込む際のチャンク戦略を指定
  - パラメータで `ragKnowledgeBaseAdvancedParsing` を `true` に指定すると、`anthropic.claude-3-sonnet-20240229-v1:0` モデルを使用したプロンプトでパース指示が可能 ([`packages/cdk/lib/stack-input.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/stack-input.ts))
  - `ragKnowledgeBaseAdvancedParsing` のデフォルトは false ([`packages/cdk/lib/stack-input.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/stack-input.ts))
  - その他、チャンク戦略を変更する場合のサンプルコードがコメント内に記載されている
    - セマンティックチャンク
    - 階層チャンク
    - 標準チャンク
- ナレッジベース: 前述の Bedrock ナレッジベースの [ナレッジベース ID](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-bedrock-knowledgebase.html#aws-resource-bedrock-knowledgebase-return-values) を指定

##### データソースのパース指示用プロンプト

`ragKnowledgeBaseAdvancedParsing` を `true` に指定した場合、デフォルトでは以下のパース指示プロンプトが設定されます。コメントにも記載のある通り、利用環境によってプロンプトを変えることで精度向上が期待できます。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
// parsingConfiguration で PDF ファイルの中に埋め込まれている画像やグラフや表を読み取る機能がある。
// 読み取る際のプロンプトは任意のものが定義できる。以下に const として定義する。利用環境によってプロンプトを変更することで、より高い精度を期待できる。
// https://docs.aws.amazon.com/bedrock/latest/userguide/kb-chunking-parsing.html#kb-advanced-parsing
const PARSING_PROMPT = `ドキュメントに含まれる画像やグラフや表などの Image コンテンツからテキストを書き写して、コードブロックではないMarkdown構文で出力してください。以下の手順に従ってください：

1. 提供されたページを注意深く調べてください。

2. ページに存在するすべての要素を特定してください。これには見出し、本文、脚注、表、視覚化、キャプション、ページ番号などが含まれます。

3. Markdown構文のフォーマットを使用して出力してください :
- 見出し：主見出しには#、セクションには##、サブセクションには###など
- リスト：箇条書きには* または -、番号付きリストには1. 2. 3.
- 繰り返しは避けてください

4. 要素が Visualization の場合：
- 自然言語で詳細な説明を提供してください
- 説明を提供した後、Visualization 内のテキストは転写しないでください

5. 要素が表の場合：
- Markdownの表を作成し、すべての行が同じ列数を持つようにしてください
- セルの配置をできるだけ忠実に維持してください
- 表を複数の表に分割しないでください
- 結合されたセルが複数の行や列にまたがる場合、テキストを左上のセルに配置し、他のセルには ' ' を出力してください
- 列の区切りには | を使用し、ヘッダー行の区切りには |-|-| を使用してください
- セルに複数の項目がある場合、別々の行にリストしてください
- 表にサブヘッダーがある場合、サブヘッダーをヘッダーから別の行で分離してください

6. 要素が段落の場合：
- 各テキスト要素を表示されているとおりに正確に転写してください

7. 要素がヘッダー、フッター、脚注、ページ番号の場合：
- 各テキスト要素を表示されているとおりに正確に転写してください

出力例：

Y軸に「売上高（$百万）」、X軸に「年」とラベル付けされた年間売上高を示す棒グラフ。グラフには2018年（$12M）、2019年（$18M）、2020年（$8M）、2021年（$22M）の棒がある。
図3：このグラフは年間売上高を百万ドル単位で示しています。2020年はCOVID-19パンデミックの影響で大幅に減少しました。

年次報告書
財務ハイライト
収益：$40M
利益：$12M
EPS：$1.25
| | 12月31日終了年度 | |

2021	2022
キャッシュフロー：
営業活動	$ 46,327	$ 46,752
投資活動	(58,154)	(37,601)
財務活動	6,291	9,718`;
```

#### RagKnowledgeBaseStack > BucketDeployment コンストラクト

BucketDeployment は S3 バケットへのデプロイ (ファイルアップロード) を指定します。
以下のソースコードが BucketDeployment の定義です。

```typescript:packages/cdk/lib/cloud-front-waf-stack.ts (抜粋)
    new s3Deploy.BucketDeployment(this, 'DeployDocs', {
      sources: [s3Deploy.Source.asset('./rag-docs')],
      destinationBucket: dataSourceBucket,
      // 以前の設定で同 Bucket にアクセスログが残っている可能性があるため、この設定は残す
      exclude: ['AccessLogs/*', 'logs*'],
    });
```

この中では、データソース S3 バケットに、[`./rag-docs`](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main/packages/cdk/rag-docs/docs) 配下のローカルファイルをアップロードすることを指定しています。誤ってログ用のフォルダにファイルを送信しないような設定も入っています。

以上です。
今回は RAG の設定ということもあり、相当長くなりました。
次回は GenU 内の `WebSearchAgentStack` スタックを解説していきたいと思います。

## (参考) GenU のバックエンド (CDK) 詳細解説投稿一覧

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)
- [⑥RagKnowledgeBaseStack スタックの解説](https://qiita.com/siruko/items/1223c9d22e73168a8809)
- [⑦WebSearchAgentStack スタックの解説](https://qiita.com/siruko/items/aef0a9599df60d47eb1e)
- [⑧GuardrailStack スタックの解説](https://qiita.com/siruko/items/d16e9fe27df4673d7554)
- [⑨GenerativeAiUseCasesStack > Auth スタックの解説](https://qiita.com/siruko/items/0c14040a1af132f42382)
- [⑩GenerativeAiUseCasesStack > Database, Api スタックの解説](https://qiita.com/siruko/items/5f249f11847f3829c18a)
- [⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの解説](https://qiita.com/siruko/items/1063daa2efe0b374a3fc)
- [⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの解説](https://qiita.com/siruko/items/73d6006004caf8648594)
- [⑬DashBoard スタックの解説](https://qiita.com/siruko/items/73a75c7d0146d12ecbc3)
- [⑭GenU の Outputs の解説](https://qiita.com/siruko/items/afc14128a5b1a15ab69c)