GenU のバックエンド (CDK) 詳細解説　 ⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの確認

## はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、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)
- [⑥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)

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

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

第 9 回から GenU 内の本丸である `GenerativeAiUseCasesStack` スタックを解説しています。

### GenerativeAiUseCasesStack スタック

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/f11bf13a-ae6f-4599-9b18-f4da0941c6e7.png)

- `Auth`
- `Database`
- `Api`
- `CommonWebAcl`
- `Web`
- `Rag`
- `RagKnowledgeBase`
- `UseCaseBuilder`
- `Transcribe`

今回は `CommonWebAcl`, `Web`, `Rag` リソースを解説していきます。

#### GenerativeAiUseCasesStack > CommonWebAcl (WAF) リソース

CommonWebAcl (WAF) リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/c3dc447d-fad9-4a05-bf7a-8f269bc298d8.png)

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

```typescript:packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
    // WAF
    if (
      params.allowedIpV4AddressRanges ||
      params.allowedIpV6AddressRanges ||
      params.allowedCountryCodes
    ) {
      const regionalWaf = new CommonWebAcl(this, 'RegionalWaf', {
        scope: 'REGIONAL',
        allowedIpV4AddressRanges: params.allowedIpV4AddressRanges,
        allowedIpV6AddressRanges: params.allowedIpV6AddressRanges,
        allowedCountryCodes: params.allowedCountryCodes,
      });
      new CfnWebACLAssociation(this, 'ApiWafAssociation', {
        resourceArn: api.api.deploymentStage.stageArn,
        webAclArn: regionalWaf.webAclArn,
      });
      new CfnWebACLAssociation(this, 'UserPoolWafAssociation', {
        resourceArn: auth.userPool.userPoolArn,
        webAclArn: regionalWaf.webAclArn,
      });
    }
```

      params.allowedIpV4AddressRanges ||
      params.allowedIpV6AddressRanges ||
      params.allowedCountryCodes

CommonWebAcl リソースの実体は [`packages/cdk/lib/construct/common-web-acl.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/a3002c273332bf8386c90f547d507eee1a727509/packages/cdk/lib/construct/common-web-acl.ts) にあります。

スタック作成時のパラメータ `allowedIpV4AddressRanges`, `allowedIpV6AddressRanges`, `allowedCountryCodes` (デフォルト値は 全て null) のいずれかに値がある場合、CommonWebAcl を作成します。

CommonWebAcl のコードは[CloudFrontWafStack スタックですでに解説済](https://qiita.com/siruko/items/30439576ee7c63165d21#cloudfrontwafstack--commonwebacl-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%BB%E3%83%83%E3%83%88) ですが、CloudFrontWafStack スタックとの違いは、`scope: 'CLOUDFRONT'` なのか `scope: 'REGIONAL'` なのかの違いになります。

つまり、`CloudFrontWafStack` 内ではグローバルの WAF 設定、`GenerativeAiUseCasesStack` 内ではリージョナルの WAF 設定を行っています。

#### GenerativeAiUseCasesStack > Web リソース

Web リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/feb4c89b-e336-4ea7-a070-07fef89cef11.png)

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

```typescript:packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
    // Web Frontend
    const web = new Web(this, 'Api', {
      // Auth
      userPoolId: auth.userPool.userPoolId,
      userPoolClientId: auth.client.userPoolClientId,
      idPoolId: auth.idPool.identityPoolId,
      selfSignUpEnabled: params.selfSignUpEnabled,
      samlAuthEnabled: params.samlAuthEnabled,
      samlCognitoDomainName: params.samlCognitoDomainName,
      samlCognitoFederatedIdentityProviderName:
        params.samlCognitoFederatedIdentityProviderName,
      // Backend
      apiEndpointUrl: api.api.url,
      predictStreamFunctionArn: api.predictStreamFunction.functionArn,
      ragEnabled: params.ragEnabled,
      ragKnowledgeBaseEnabled: params.ragKnowledgeBaseEnabled,
      agentEnabled: params.agentEnabled || params.agents.length > 0,
      flows: params.flows,
      flowStreamFunctionArn: api.invokeFlowFunction.functionArn,
      optimizePromptFunctionArn: api.optimizePromptFunction.functionArn,
      webAclId: props.webAclId,
      modelRegion: api.modelRegion,
      modelIds: api.modelIds,
      imageGenerationModelIds: api.imageGenerationModelIds,
      endpointNames: api.endpointNames,
      agentNames: api.agentNames,
      inlineAgents: params.inlineAgents,
      useCaseBuilderEnabled: params.useCaseBuilderEnabled,
      // Frontend
      hiddenUseCases: params.hiddenUseCases,
      // Custom Domain
      cert: props.cert,
      hostName: params.hostName,
      domainName: params.domainName,
      hostedZoneId: params.hostedZoneId,
    });
```

Web リソースの実体は [`packages/cdk/lib/construct/web.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/a3002c273332bf8386c90f547d507eee1a727509/packages/cdk/lib/construct/web.ts) にあります。

```typescript:packages/cdk/lib/construct/web.ts
import { Stack, RemovalPolicy, CfnResource } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  CloudFrontToS3,
  CloudFrontToS3Props,
} from '@aws-solutions-constructs/aws-cloudfront-s3';
import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';
import { NodejsBuild } from 'deploy-time-build';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { Flow, HiddenUseCases } from 'generative-ai-use-cases-jp';
import { ComputeType } from 'aws-cdk-lib/aws-codebuild';

export interface WebProps {
  apiEndpointUrl: string;
  userPoolId: string;
  userPoolClientId: string;
  idPoolId: string;
  predictStreamFunctionArn: string;
  ragEnabled: boolean;
  ragKnowledgeBaseEnabled: boolean;
  agentEnabled: boolean;
  flows?: Flow[];
  flowStreamFunctionArn: string;
  optimizePromptFunctionArn: string;
  selfSignUpEnabled: boolean;
  webAclId?: string;
  modelRegion: string;
  modelIds: string[];
  imageGenerationModelIds: string[];
  endpointNames: string[];
  samlAuthEnabled: boolean;
  samlCognitoDomainName?: string | null;
  samlCognitoFederatedIdentityProviderName?: string | null;
  agentNames: string[];
  inlineAgents: boolean;
  cert?: ICertificate;
  hostName?: string | null;
  domainName?: string | null;
  hostedZoneId?: string | null;
  useCaseBuilderEnabled: boolean;
  hiddenUseCases: HiddenUseCases;
}

export class Web extends Construct {
  public readonly distribution: Distribution;

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

    const commonBucketProps: s3.BucketProps = {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      autoDeleteObjects: true,
      removalPolicy: RemovalPolicy.DESTROY,
      objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
      enforceSSL: true,
    };

    const cloudFrontToS3Props: CloudFrontToS3Props = {
      insertHttpSecurityHeaders: false,
      loggingBucketProps: commonBucketProps,
      bucketProps: commonBucketProps,
      cloudFrontLoggingBucketProps: commonBucketProps,
      cloudFrontLoggingBucketAccessLogBucketProps: commonBucketProps,
      cloudFrontDistributionProps: {
        errorResponses: [
          {
            httpStatus: 403,
            responseHttpStatus: 200,
            responsePagePath: '/index.html',
          },
          {
            httpStatus: 404,
            responseHttpStatus: 200,
            responsePagePath: '/index.html',
          },
        ],
      },
    };

    if (
      props.cert &&
      props.hostName &&
      props.domainName &&
      props.hostedZoneId
    ) {
      cloudFrontToS3Props.cloudFrontDistributionProps.certificate = props.cert;
      cloudFrontToS3Props.cloudFrontDistributionProps.domainNames = [
        `${props.hostName}.${props.domainName}`,
      ];
    }

    const { cloudFrontWebDistribution, s3BucketInterface } = new CloudFrontToS3(
      this,
      'Web',
      cloudFrontToS3Props
    );

    if (
      props.cert &&
      props.hostName &&
      props.domainName &&
      props.hostedZoneId
    ) {
      // DNS record for custom domain
      const hostedZone = HostedZone.fromHostedZoneAttributes(
        this,
        'HostedZone',
        {
          hostedZoneId: props.hostedZoneId,
          zoneName: props.domainName,
        }
      );
      new ARecord(this, 'ARecord', {
        zone: hostedZone,
        recordName: props.hostName,
        target: RecordTarget.fromAlias(
          new CloudFrontTarget(cloudFrontWebDistribution)
        ),
      });
    }

    if (props.webAclId) {
      const existingCloudFrontWebDistribution = cloudFrontWebDistribution.node
        .defaultChild as CfnDistribution;
      existingCloudFrontWebDistribution.addPropertyOverride(
        'DistributionConfig.WebACLId',
        props.webAclId
      );
    }

    const build = new NodejsBuild(this, 'BuildWeb', {
      assets: [
        {
          path: '../../',
          exclude: [
            '.git',
            '.github',
            '.gitignore',
            '.prettierignore',
            '.prettierrc.json',
            '*.md',
            'LICENSE',
            'docs',
            'imgs',
            'setup-env.sh',
            'node_modules',
            'prompt-templates',
            'packages/cdk/**/*',
            '!packages/cdk/cdk.json',
            'packages/web/dist',
            'packages/web/dev-dist',
            'packages/web/node_modules',
            'browser-extension',
          ],
        },
      ],
      destinationBucket: s3BucketInterface,
      distribution: cloudFrontWebDistribution,
      outputSourceDirectory: './packages/web/dist',
      buildCommands: ['npm ci', 'npm run web:build'],
      buildEnvironment: {
        NODE_OPTIONS: '--max-old-space-size=4096', // デプロイ時のCodeBuildのメモリを設定
        VITE_APP_API_ENDPOINT: props.apiEndpointUrl,
        VITE_APP_REGION: Stack.of(this).region,
        VITE_APP_USER_POOL_ID: props.userPoolId,
        VITE_APP_USER_POOL_CLIENT_ID: props.userPoolClientId,
        VITE_APP_IDENTITY_POOL_ID: props.idPoolId,
        VITE_APP_PREDICT_STREAM_FUNCTION_ARN: props.predictStreamFunctionArn,
        VITE_APP_RAG_ENABLED: props.ragEnabled.toString(),
        VITE_APP_RAG_KNOWLEDGE_BASE_ENABLED:
          props.ragKnowledgeBaseEnabled.toString(),
        VITE_APP_AGENT_ENABLED: props.agentEnabled.toString(),
        VITE_APP_FLOWS: JSON.stringify(props.flows || []),
        VITE_APP_FLOW_STREAM_FUNCTION_ARN: props.flowStreamFunctionArn,
        VITE_APP_OPTIMIZE_PROMPT_FUNCTION_ARN: props.optimizePromptFunctionArn,
        VITE_APP_SELF_SIGN_UP_ENABLED: props.selfSignUpEnabled.toString(),
        VITE_APP_MODEL_REGION: props.modelRegion,
        VITE_APP_MODEL_IDS: JSON.stringify(props.modelIds),
        VITE_APP_IMAGE_MODEL_IDS: JSON.stringify(props.imageGenerationModelIds),
        VITE_APP_ENDPOINT_NAMES: JSON.stringify(props.endpointNames),
        VITE_APP_SAMLAUTH_ENABLED: props.samlAuthEnabled.toString(),
        VITE_APP_SAML_COGNITO_DOMAIN_NAME: props.samlCognitoDomainName ?? '',
        VITE_APP_SAML_COGNITO_FEDERATED_IDENTITY_PROVIDER_NAME:
          props.samlCognitoFederatedIdentityProviderName ?? '',
        VITE_APP_AGENT_NAMES: JSON.stringify(props.agentNames),
        VITE_APP_INLINE_AGENTS: props.inlineAgents.toString(),
        VITE_APP_USE_CASE_BUILDER_ENABLED:
          props.useCaseBuilderEnabled.toString(),
        VITE_APP_HIDDEN_USE_CASES: JSON.stringify(props.hiddenUseCases),
      },
    });
    // コンピューティングリソースを増強
    (
      build.node.findChild('Project').node.defaultChild as CfnResource
    ).addPropertyOverride('Environment.ComputeType', ComputeType.MEDIUM);

    this.distribution = cloudFrontWebDistribution;
  }
}
```

この中では、Cloudfront, S3 を生成し、Vite アプリケーションをビルドした資材をデプロイしています。

- [`CloudFrontToS3`](https://docs.aws.amazon.com/solutions/latest/constructs/aws-cloudfront-s3.html)
  - `CloudFrontWafStack` で生成した WebACL があれば Cloudfront に設定する
- 以下をすべて満たす場合のみ、独自ドメインアクセス用の [`ARecord`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_route53.ARecord.html) を作成
  - `CloudFrontWafStack` リソースで作成した証明書
  - スタック作成時のパラメータ `hostName` (デフォルト値は null)
  - スタック作成時のパラメータ `domainName` (デフォルト値は null)
  - スタック作成時のパラメータ `hostedZoneId` (デフォルト値は null)

CloudFrontToS3 は AWS Solutions Constructs の 1 つのアーキテクチャパターンであり、S3 をオリジンとした CloudFront を簡単に生成することができます。

> AWS Solutions Constructs (Constructs)は、AWS Cloud Development Kit (AWS CDK)のオープンソース拡張であり、予測可能で反復可能なインフラストラクチャを作成するために、コードで迅速にソリューションを定義するためのマルチサービス、優れたアーキテクチャパターンを提供します。 Constructs の目標は、開発者がアーキテクチャのパターンベースの定義を使用して、あらゆる規模のソリューションを構築する経験を加速することです。

また、[Deploy-time Build](https://github.com/tmokmss/deploy-time-build) の `NodejsBuild` を用いて Vite アプリケーションのビルドを行っています。

- デプロイ先バケットに `CloudFrontToS3` で生成したデプロイ先 S3 バケットを指定
- CloudFront ディストリビューションに `CloudFrontToS3` で生成したディストリビューションを指定
- 実行ディレクトリは `packages/cdk/`
- 実行コマンドは `npm ic` および `npm run web:build` コマンド
  - `web:build` コマンドは `VITE_APP_VERSION=${npm_package_version} npm -w packages/web run build --` を実行する
- ビルド時の環境変数に以下を指定
  - `NODE_OPTIONS`: `--max-old-space-size=4096`
  - `VITE_APP_API_ENDPOINT`: `Api` リソースの API エンドポイント
  - `VITE_APP_REGION`: 現在のリージョン
  - `VITE_APP_USER_POOL_ID`: `Auth` リソースの `userPoolId`
  - `VITE_APP_USER_POOL_CLIENT_ID`: `Auth` リソースの `userPoolClientId`
  - `VITE_APP_IDENTITY_POOL_ID`: `Auth` リソースの `idPoolId`
  - `VITE_APP_PREDICT_STREAM_FUNCTION_ARN`: `Api` リソースの `predictStreamFunction` 関数の ARN
  - `VITE_APP_RAG_ENABLED`: スタック作成時のパラメータ `ragEnabled` (デフォルト値は false)
  - `VITE_APP_RAG_KNOWLEDGE_BASE_ENABLED`: スタック作成時のパラメータ `ragKnowledgeBaseEnabled` (デフォルト値は false)
  - `VITE_APP_AGENT_ENABLED`: スタック作成時のパラメータ `agentEnabled` (デフォルト値は false)
  - `VITE_APP_FLOWS`: スタック作成時のパラメータ `flows` (デフォルト値は false)
  - `VITE_APP_FLOW_STREAM_FUNCTION_ARN`: `Api` リソースの `invokeFlowFunction` 関数の ARN
  - `VITE_APP_OPTIMIZE_PROMPT_FUNCTION_ARN`: `Api` リソースの `optimizePromptFunction` 関数の ARN
  - `VITE_APP_SELF_SIGN_UP_ENABLED`: スタック作成時のパラメータ `selfSignUpEnabled` (デフォルト値は true)
  - `VITE_APP_MODEL_REGION`: スタック作成時のパラメータ `modelRegion` (デフォルト値は us-east-1)
  - `VITE_APP_MODEL_IDS`: スタック作成時のパラメータ `modelIds` (デフォルト値は以下の配列)
    - `us.anthropic.claude-3-5-sonnet-20241022-v2:0`
    - `us.anthropic.claude-3-5-haiku-20241022-v1:0`
    - `us.amazon.nova-pro-v1:0`
    - `us.amazon.nova-lite-v1:0`
    - `us.amazon.nova-micro-v1:0`
  - `VITE_APP_IMAGE_MODEL_IDS`: スタック作成時のパラメータ `imageGenerationModelIds` (デフォルト値は以下の配列)
    - `amazon.nova-canvas-v1:0`
  - `VITE_APP_ENDPOINT_NAMES`: スタック作成時のパラメータ `endpointNames` (デフォルト値は空配列)
  - `VITE_APP_SAMLAUTH_ENABLED`: スタック作成時のパラメータ `samlAuthEnabled` (デフォルト値は false)
  - `VITE_APP_SAML_COGNITO_DOMAIN_NAME`: スタック作成時のパラメータ `samlCognitoDomainName` (デフォルト値は null)
  - `VITE_APP_SAML_COGNITO_FEDERATED_IDENTITY_PROVIDER_NAME`: スタック作成時のパラメータ `samlCognitoFederatedIdentityProviderName` (デフォルト値は null)
  - `VITE_APP_AGENT_NAMES`: 以下のいずれか (上を優先)
    - `AgentStack` で生成した Web 検索エージェントおよびコードインタプリタの `displayName`
    - スタック作成時のパラメータ `agents` の `displayName` (デフォルト値は空配列)
  - `VITE_APP_INLINE_AGENTS`: スタック作成時のパラメータ `inlineAgents` (デフォルト値は false)
  - `VITE_APP_USE_CASE_BUILDER_ENABLED`: スタック作成時のパラメータ `useCaseBuilderEnabled` (デフォルト値は true)
  - `VITE_APP_HIDDEN_USE_CASES`: スタック作成時のパラメータ `hiddenUseCases` (デフォルト値は空オブジェクト)
- [CodeBuld Project](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codebuild.Project.html) のパラメータ `Environment.ComputeType` を [`ComputeType.MEDIUM`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codebuild.ComputeType.html) で上書き

#### GenerativeAiUseCasesStack > Rag リソース

Rag リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/ef1667b9-1713-43e4-838e-20d2dbb5ec90.png)

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

```typescript:packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
    // RAG
    if (params.ragEnabled) {
      const rag = new Rag(this, 'Rag', {
        envSuffix: params.env,
        kendraIndexArnInCdkContext: params.kendraIndexArn,
        kendraDataSourceBucketName: params.kendraDataSourceBucketName,
        kendraIndexScheduleEnabled: params.kendraIndexScheduleEnabled,
        kendraIndexScheduleCreateCron: params.kendraIndexScheduleCreateCron,
        kendraIndexScheduleDeleteCron: params.kendraIndexScheduleDeleteCron,
        userPool: auth.userPool,
        api: api.api,
      });

      // File API から data source の Bucket のファイルをダウンロードできるようにする
      // 既存の Kendra を import している場合、data source が S3 ではない可能性がある
      // その際は rag.dataSourceBucketName が undefined になって権限は付与されない
      if (rag.dataSourceBucketName) {
        api.allowDownloadFile(rag.dataSourceBucketName);
      }
    }
```

Rag リソースの実体は [`packages/cdk/lib/construct/rag.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/56583580fbc767c70ca451a09cc98ce7c299b998/packages/cdk/lib/construct/rag.ts) にあります。
スタック作成時のパラメータ `ragEnabled` (デフォルト値は false) が true の場合、以下のコードを実行し RAG を作成します。

```typescript:packages/cdk/lib/construct/rag.ts
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as kendra from 'aws-cdk-lib/aws-kendra';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3Deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions';
import * as stepfunctionsTasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
import { Construct } from 'constructs';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import { Duration, Token, Arn, RemovalPolicy } from 'aws-cdk-lib';
import {
  AuthorizationType,
  CognitoUserPoolsAuthorizer,
  LambdaIntegration,
  RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';

const KENDRA_STATE_CFN_PARAMETER_NAME = 'kendraState';

export interface RagProps {
  // Context Params
  envSuffix: string;
  kendraIndexArnInCdkContext?: string | null;
  kendraDataSourceBucketName?: string | null;
  kendraIndexScheduleEnabled: boolean;
  kendraIndexScheduleCreateCron?: IndexScheduleCron | null;
  kendraIndexScheduleDeleteCron?: IndexScheduleCron | null;

  // Resource
  userPool: UserPool;
  api: RestApi;
}

export interface IndexScheduleCron {
  minute: string;
  hour: string;
  month: string;
  weekDay: string;
}

class KendraIndexWithCfnParameter extends kendra.CfnIndex {
  attrId: string;
  attrArn: string;

  constructor(
    scope: Construct,
    id: string,
    props: kendra.CfnIndexProps,
    kendraSwitchCfnCondition: cdk.CfnCondition
  ) {
    super(scope, id, props);

    this.attrId = cdk.Fn.conditionIf(
      kendraSwitchCfnCondition.logicalId,
      this.attrId, // kendraがオンの場合は、attrIdをそのまま返す
      `` // kendraがオフの場合は、空文字列を設定しておく
    ).toString();

    this.attrArn = cdk.Fn.conditionIf(
      kendraSwitchCfnCondition.logicalId,
      this.attrArn, // kendraがオンの場合は、attrArnをそのまま返す
      `arn:aws:kendra:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:index/` // kendraがオフの場合は、index/以降を空文字列にする（IAMの許可をさせない）
    ).toString();
  }
}

class KendraDataSourceWithCfnParameter extends kendra.CfnDataSource {
  attrId: string;
  attrArn: string;

  constructor(
    scope: Construct,
    id: string,
    props: kendra.CfnDataSourceProps,
    kendraSwitchCfnCondition: cdk.CfnCondition
  ) {
    super(scope, id, props);

    this.attrId = cdk.Fn.conditionIf(
      kendraSwitchCfnCondition.logicalId,
      this.attrId, // kendraがオンの場合は、attrIdをそのまま返す
      `` // kendraがオフの場合は、空文字列を設定しておく
    ).toString();

    this.attrArn = cdk.Fn.conditionIf(
      kendraSwitchCfnCondition.logicalId,
      this.attrArn, // kendraがオンの場合は、attrArnをそのまま返す
      `arn:aws:kendra:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:index/*/data-source/` // kendraがオフの場合は、index/以降を空文字列にする（IAMの許可をさせない）
    ).toString();
  }
}

/**
 * RAG を実行するためのリソースを作成する
 */
export class Rag extends Construct {
  public readonly dataSourceBucketName?: string;

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

    const {
      envSuffix,
      kendraIndexArnInCdkContext,
      kendraDataSourceBucketName,
      kendraIndexScheduleEnabled,
      kendraIndexScheduleCreateCron,
      kendraIndexScheduleDeleteCron,
    } = props;

    let kendraIndexArn: string;
    let kendraIndexId: string;
    let dataSourceBucket: s3.IBucket | null = null;

    if (kendraIndexArnInCdkContext) {
      // 既存の Kendra Index を利用する場合
      kendraIndexArn = kendraIndexArnInCdkContext!;
      kendraIndexId = Arn.extractResourceName(
        kendraIndexArnInCdkContext,
        'index'
      );
      // 既存の S3 データソースを利用する場合は、バケット名からオブジェクトを生成
      if (kendraDataSourceBucketName) {
        dataSourceBucket = s3.Bucket.fromBucketName(
          this,
          'DataSourceBucket',
          kendraDataSourceBucketName
        );
      }
    } else {
      // 新規に Kendra Index を作成する場合
      const indexRole = new iam.Role(this, 'KendraIndexRole', {
        assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'),
      });

      indexRole.addToPolicy(
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          resources: ['*'],
          actions: ['s3:GetObject'],
        })
      );

      indexRole.addManagedPolicy(
        iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess')
      );

      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,
        }
      );

      // .pdf や .txt などのドキュメントを格納する S3 Bucket
      dataSourceBucket = new s3.Bucket(this, 'DataSourceBucket', {
        blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
        encryption: s3.BucketEncryption.S3_MANAGED,
        autoDeleteObjects: true,
        removalPolicy: RemovalPolicy.DESTROY,
        objectOwnership: s3.ObjectOwnership.OBJECT_WRITER,
        serverAccessLogsBucket: accessLogsBucket,
        serverAccessLogsPrefix: 'AccessLogs/',
        enforceSSL: true,
      });

      // /kendra/docs ディレクトリを Bucket にアップロードする
      new s3Deploy.BucketDeployment(this, 'DeployDocs', {
        sources: [s3Deploy.Source.asset('./rag-docs')],
        destinationBucket: dataSourceBucket,
        // 以前の設定で同 Bucket にアクセスログが残っている可能性があるため、この設定は残す
        exclude: ['AccessLogs/*', 'logs*', 'docs/bedrock-ug.pdf.metadata.json'],
      });

      let index: kendra.CfnIndex;
      const indexProps: kendra.CfnIndexProps = {
        name: `generative-ai-use-cases-index${envSuffix}`,
        edition: 'DEVELOPER_EDITION',
        roleArn: indexRole.roleArn,

        // トークンベースのアクセス制御を実施
        // 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-index.html#cfn-kendra-index-usercontextpolicy
        userContextPolicy: 'USER_TOKEN',

        // 認可に利用する Cognito の情報を設定
        userTokenConfigurations: [
          {
            jwtTokenTypeConfiguration: {
              keyLocation: 'URL',
              userNameAttributeField: 'cognito:username',
              groupAttributeField: 'cognito:groups',
              url: `${props.userPool.userPoolProviderUrl}/.well-known/jwks.json`,
            },
          },
        ],
      };

      let kendraIsOnCfnCondition;
      if (kendraIndexScheduleEnabled) {
        // Cloudfomation Parameterの読み込み
        const kendraStateCfnParameter = new cdk.CfnParameter(
          scope,
          KENDRA_STATE_CFN_PARAMETER_NAME,
          {
            // NOTE contructの名前が付加されないように、thisではなくscopeを指定する
            type: 'String',
            description:
              'parameter to create kendra index. on: create kendra index, off: delete kendra index.',
            allowedValues: ['on', 'off'],
            default: 'on',
          }
        );
        kendraIsOnCfnCondition = new cdk.CfnCondition(
          scope,
          'IsKendraOnCondition',
          {
            expression: cdk.Fn.conditionEquals(
              kendraStateCfnParameter.valueAsString,
              'on'
            ),
          }
        );

        index = new KendraIndexWithCfnParameter(
          this,
          'KendraIndex',
          indexProps,
          kendraIsOnCfnCondition
        );
        index.cfnOptions.condition = kendraIsOnCfnCondition; // Cfn Parameterに応じて、リソースをオンオフする

        kendraIndexArn = index.attrArn;
        kendraIndexId = index.attrId;
      } else {
        index = new kendra.CfnIndex(this, 'KendraIndex', indexProps);

        kendraIndexArn = Token.asString(index.getAtt('Arn'));
        kendraIndexId = index.ref;
      }

      const s3DataSourceRole = new iam.Role(this, 'DataSourceRole', {
        assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'),
      });

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

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

      s3DataSourceRole.addToPolicy(
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          resources: [index.attrArn],
          actions: ['kendra:BatchPutDocument', 'kendra:BatchDeleteDocument'],
        })
      );

      let dataSource: kendra.CfnDataSource;
      const dataSourceProps: kendra.CfnDataSourceProps = {
        indexId: index.attrId,
        type: 'S3',
        name: 's3-data-source',
        roleArn: s3DataSourceRole.roleArn,
        languageCode: 'ja',
        dataSourceConfiguration: {
          s3Configuration: {
            bucketName: dataSourceBucket.bucketName,
            inclusionPrefixes: ['docs'],
          },
        },
      };
      if (kendraIndexScheduleEnabled) {
        dataSource = new KendraDataSourceWithCfnParameter(
          this,
          'S3DataSource',
          dataSourceProps,
          kendraIsOnCfnCondition as cdk.CfnCondition
        );
        dataSource.cfnOptions.condition = kendraIsOnCfnCondition; // Cfn Parameterに応じて、リソースをオンオフする
      } else {
        dataSource = new kendra.CfnDataSource(
          this,
          'S3DataSource',
          dataSourceProps
        );
      }
      dataSource.addDependency(index);

      if (kendraIndexScheduleEnabled) {
        if (kendraIndexScheduleCreateCron) {
          const taskStartDataSourceSyncJob =
            new stepfunctionsTasks.CallAwsService(
              this,
              'TaskStartDataSourceSyncJob',
              {
                service: 'kendra',
                action: 'startDataSourceSyncJob',
                parameters: {
                  IndexId: index.attrId,
                  Id: dataSource.attrId,
                },
                iamResources: [
                  // NOTE インデックス・データソースの両方に対する権限が必要
                  index.attrArn,
                  dataSource.attrArn,
                ],
              }
            );

          const definitionStartDataSourceSyncJob = stepfunctions.Chain.start(
            taskStartDataSourceSyncJob
          );

          const stateMachineStartDataSourceSyncJob =
            new stepfunctions.StateMachine(
              this,
              'StepFunctionsStateMachineStartDataSourceSyncJob',
              {
                definitionBody: stepfunctions.DefinitionBody.fromChainable(
                  definitionStartDataSourceSyncJob
                ),
                timeout: cdk.Duration.minutes(180),
              }
            );

          // Kendra On用のStep Functions
          const taskUpdateCloudformationStackWithKendraOn =
            new stepfunctionsTasks.CallAwsService(
              this,
              'TaskUpdateCloudformationStackWithKendraOn',
              {
                service: 'cloudformation',
                action: 'updateStack',
                parameters: {
                  StackName: cdk.Stack.of(this).stackName,
                  UsePreviousTemplate: true,
                  Parameters: [
                    {
                      ParameterKey: KENDRA_STATE_CFN_PARAMETER_NAME,
                      ParameterValue: 'on',
                    },
                  ],
                  Capabilities: ['CAPABILITY_IAM'],
                },
                iamResources: [cdk.Stack.of(this).stackId], // NOTE stackId (arn:aws:cloudformation:ap-northeast-1:123456789012:stack/myStack/i-01234567890abcdef0) can be used an Resource ARN
              }
            );

          const taskCheckCloudformationState =
            new stepfunctionsTasks.CallAwsService(
              this,
              'TaskCheckCloudformationState',
              {
                service: 'cloudformation',
                action: 'describeStacks',
                parameters: {
                  StackName: cdk.Stack.of(this).stackName,
                },
                iamResources: [cdk.Stack.of(this).stackId], // NOTE stackId (arn:aws:cloudformation:ap-northeast-1:123456789012:stack/myStack/i-01234567890abcdef0) can be used an Resource ARN
              }
            );

          const taskCallStartDataSourceSyncJob =
            new stepfunctionsTasks.StepFunctionsStartExecution(
              this,
              'TaskCallStateMachineStartDataSourceSyncJob',
              {
                stateMachine: stateMachineStartDataSourceSyncJob,
                integrationPattern: stepfunctions.IntegrationPattern.RUN_JOB,
              }
            );

          const definitionKendraOn = stepfunctions.Chain.start(
            taskUpdateCloudformationStackWithKendraOn
          )
            .next(taskCheckCloudformationState)
            .next(
              new stepfunctions.Choice(
                this,
                'TaskChoiceWithCloudformationState'
              )
                .when(
                  stepfunctions.Condition.stringEquals(
                    '$.Stacks[0].StackStatus',
                    'UPDATE_IN_PROGRESS'
                  ), // 完了するまでループ
                  new stepfunctions.Wait(this, 'TaskWaitCloudformationChange', {
                    time: stepfunctions.WaitTime.duration(
                      cdk.Duration.minutes(5)
                    ),
                  }).next(taskCheckCloudformationState) // ループ
                )
                .otherwise(
                  // 完了したら、次ステップ
                  // データソースSyncのStateMachineを呼び出す
                  taskCallStartDataSourceSyncJob
                )
            );

          const stateMachineKendraOn = new stepfunctions.StateMachine(
            this,
            'StepFunctionsStateMachineKendraOn',
            {
              definitionBody:
                stepfunctions.DefinitionBody.fromChainable(definitionKendraOn),
              timeout: cdk.Duration.minutes(180),
            }
          );

          // cronJobKendraOn
          new events.Rule(this, 'CronJobKendraOn', {
            schedule: events.Schedule.cron({
              minute: kendraIndexScheduleCreateCron.minute,
              hour: kendraIndexScheduleCreateCron.hour,
              month: kendraIndexScheduleCreateCron.month,
              weekDay: kendraIndexScheduleCreateCron.weekDay,
            }), // NOTE UTC時間で指定
            targets: [new targets.SfnStateMachine(stateMachineKendraOn, {})],
          });
        }

        if (kendraIndexScheduleDeleteCron) {
          // Kendra Off用のStep Function
          const taskUpdateCloudformationStackWithKendraOff =
            new stepfunctionsTasks.CallAwsService(
              this,
              'TaskUpdateCloudformationStackWithKendraOff',
              {
                service: 'cloudformation',
                action: 'updateStack',
                parameters: {
                  StackName: cdk.Stack.of(this).stackName,
                  UsePreviousTemplate: true,
                  Parameters: [
                    {
                      ParameterKey: KENDRA_STATE_CFN_PARAMETER_NAME,
                      ParameterValue: 'off',
                    },
                  ],
                  Capabilities: ['CAPABILITY_IAM'],
                },
                iamResources: [cdk.Stack.of(this).stackId],
              }
            );

          const definitionKendraOff = stepfunctions.Chain.start(
            taskUpdateCloudformationStackWithKendraOff
          );

          const stateMachineKendraOff = new stepfunctions.StateMachine(
            this,
            'StepFunctionsStateMachineKendraOff',
            {
              definitionBody:
                stepfunctions.DefinitionBody.fromChainable(definitionKendraOff),
              timeout: cdk.Duration.minutes(180),
            }
          );

          // cronJobKendraOff
          new events.Rule(this, 'CronJobKendraOff', {
            schedule: events.Schedule.cron({
              minute: kendraIndexScheduleDeleteCron.minute,
              hour: kendraIndexScheduleDeleteCron.hour,
              month: kendraIndexScheduleDeleteCron.month,
              weekDay: kendraIndexScheduleDeleteCron.weekDay,
            }), // NOTE UTC時間で指定
            targets: [new targets.SfnStateMachine(stateMachineKendraOff, {})],
          });
        }
      }
    }

    // RAG 関連の API を追加する
    // Lambda
    const queryFunction = new NodejsFunction(this, 'Query', {
      runtime: Runtime.NODEJS_LATEST,
      entry: './lambda/queryKendra.ts',
      timeout: Duration.minutes(15),
      bundling: {
        // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする
        externalModules: [],
      },
      environment: {
        INDEX_ID: kendraIndexId,
      },
    });
    queryFunction.role?.addToPrincipalPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [kendraIndexArn],
        actions: ['kendra:Query'],
      })
    );

    const retrieveFunction = new NodejsFunction(this, 'Retrieve', {
      runtime: Runtime.NODEJS_LATEST,
      entry: './lambda/retrieveKendra.ts',
      timeout: Duration.minutes(15),
      bundling: {
        // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする
        externalModules: [],
      },
      environment: {
        INDEX_ID: kendraIndexId,
      },
    });
    retrieveFunction.role?.addToPrincipalPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [kendraIndexArn],
        actions: ['kendra:Retrieve'],
      })
    );

    // API Gateway
    const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [props.userPool],
    });

    const commonAuthorizerProps = {
      authorizationType: AuthorizationType.COGNITO,
      authorizer,
    };
    const ragResource = props.api.root.addResource('rag');

    const queryResource = ragResource.addResource('query');
    // POST: /rag/query
    queryResource.addMethod(
      'POST',
      new LambdaIntegration(queryFunction),
      commonAuthorizerProps
    );

    const retrieveResource = ragResource.addResource('retrieve');
    // POST: /rag/retrieve
    retrieveResource.addMethod(
      'POST',
      new LambdaIntegration(retrieveFunction),
      commonAuthorizerProps
    );

    this.dataSourceBucketName = dataSourceBucket?.bucketName;
  }
}
```

この中では、Amazon Kendra のインデックス、データソースおよびスケジュール起動停止ジョブ、RAG 用の API を生成しています。

- スタック作成時のパラメータ `kendraIndexArn` (デフォルト値は null) に値がある場合、既存の Kendra インデックスを利用する
  - さらに、スタック作成時のパラメータ `kendraDataSourceBucketName` (デフォルト値は null) に値がある場合、既存の Kendra データソースを利用する
- スタック作成時のパラメータ `kendraIndexArn` (デフォルト値は null) に値がない場合、新規の Kendra インデックス、データソース等を作成する
  - `kendra.amazonaws.com` をプリンシパルにもつ `KendraIndexRole` を作成
    - `KendraIndexRole` に `s3:GetObject` の許可ポリシーをアタッチ
    - `KendraIndexRole` に `CloudWatchLogsFullAccess` のポリシーをアタッチ
  - アクセスログ保管用の `DataSourceAccessLogsBucket` S3 バケットを作成
    - ブロックパブリックアクセスはブロック
    - S3 マネージドキーでの暗号化
    - スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
    - [オブジェクトの所有者](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.ObjectOwnership.html): オブジェクトをアップロードしたアカウント
    - SSL 接続を強制する
  - ドキュメント格納用の `DataSourceBucket` S3 バケットを作成
    - ブロックパブリックアクセスはブロック
    - S3 マネージドキーでの暗号化
    - スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
    - [オブジェクトの所有者](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.ObjectOwnership.html): オブジェクトをアップロードしたアカウント
    - アクセスログ: アクセスログ保管用バケットの `AccessLogs/` 配下を指定
    - SSL 接続を強制する
  - ドキュメント格納用 S3 バケットに、[`./rag-docs`](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main/packages/cdk/rag-docs/docs) 配下のローカルファイルをアップロード
    - 誤ってログ用のフォルダにファイルを送信しないように除外フォルダを設定
  - [Kendra インデックスの設定値 (`kendra.CfnIndexProps`)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kendra-readme.html)を作成する
    - `name`: `generative-ai-use-cases-index[環境名]`
    - `edition`: 固定値 'DEVELOPER_EDITION'
    - `roleArn`: `KendraIndexRole` の ARN
    - `userContextPolicy`: 固定値 ['USER_TOKEN'](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-index.html#cfn-kendra-index-usercontextpolicy)
    - `userTokenConfigurations`: 以下の配列
      - `jwtTokenTypeConfiguration`: 以下のオブジェクト
        - `keyLocation`: 固定値 'URL'
        - `userNameAttributeField`: 固定値 'cognito:username'
        - `groupAttributeField`: 固定値 'cognito:groups'
        - `url`: `Auth` リソースの `userPoolProviderUrl` + `/.well-known/jwks.json`
  - Kendra インデックス `kendra.CfnIndex` を作成する
  - Kendra データソース用の `DataSourceRole` ロールを作成する
    - ドキュメント格納用 S3 バケットの `s3:ListBucket`, `s3:GetObject` を許可ポリシーをアタッチ
    - Kendra インデックスの `kendra:BatchPutDocument`, `kendra:BatchDeleteDocument` を許可ポリシーをアタッチ
  - [Kendra データソースの設定値 (`kendra.CfnDataSourceProps`)](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kendra.CfnDataSourceProps.html)を作成する
    - `indexId`: Kendra インデックスの ID
    - `type`: 固定値 'S3'
    - `name`: 固定値 's3-data-source',
    - `roleArn`: `DataSourceRole` の ARN
    - `languageCode`: 固定値 'ja',
    - `dataSourceConfiguration`:
      - `s3Configuration`:
        - `bucketName`: ドキュメント格納用 S3 バケットのバケット名
        - `inclusionPrefixes`: 固定値 ['docs']
  - Kendra データソース `kendra.CfnDataSource` を作成する
  - スタック作成時のパラメータ `kendraIndexScheduleEnabled` が true の場合、Kendra を起動停止する [`StepFunctions`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions_tasks.CallAwsService.html) と[`EventBridge Rule`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events.Rule.html) を作成する
    - スタック作成時のパラメータ `kendraIndexScheduleCreateCron` (デフォルト値は null) に cron 値が設定されている場合
      - `StepFunctionsStateMachineKendraOn` StepFunctions ステートマシンを作成する
        - ステートマシンのタイムアウトは 3 時間 (180 分)
        - `TaskUpdateCloudformationStackWithKendraOn` ジョブから開始
          - CloudFormation の `updateStack` を呼び出す
          - パラメータ `KendraState: on` を設定し Kendra インデックスを作成
        - `TaskCheckCloudformationState` ジョブに遷移
          - CloudFormation の `describeStacks` を呼び出す
        - CloudFormation の実行が完了するまで 5 分待機を繰り返す
        - `TaskCallStateMachineStartDataSourceSyncJob` ジョブに遷移
          - Kendra データソース同期する `StepFunctionsStateMachineStartDataSourceSyncJob` StepFunctions ステートマシンを実行
            - ステートマシンのタイムアウトは 3 時間 (180 分)
            - `TaskStartDataSourceSyncJob` ジョブから開始 (このジョブのみなので終了)
              - Kendra の `startDataSourceSyncJob` を呼び出す
      - `CronJobKendraOn` EventBridge Rule を作成する
        - `kendraIndexScheduleCreateCron` の値でスケジューリング
        - ターゲットに `StepFunctionsStateMachineKendraOn` StepFunctions ステートマシンを設定
    - スタック作成時のパラメータ `kendraIndexScheduleDeleteCron` (デフォルト値は null) に cron 値が設定されている場合
      - `StepFunctionsStateMachineKendraOff` StepFunctions ステートマシンを作成する
        - ステートマシンのタイムアウトは 3 時間 (180 分)
        - `TaskUpdateCloudformationStackWithKendraOff` ジョブから開始 (このジョブのみなので終了)
          - CloudFormation の `updateStack` を呼び出す
          - パラメータ `KendraState: off` を設定し Kendra インデックスを削除
      - `CronJobKendraOff` EventBridge Rule を作成する
        - `kendraIndexScheduleDeleteCron` の値でスケジューリング
        - ターゲットに `StepFunctionsStateMachineKendraOff` StepFunctions ステートマシンを設定
  - RAG 用の API を作成する
    - `Query` Lambda 関数を作成する
      - ランタイムは NodeJS の最新版
      - ソースコードは [`packages/cdk/lambda/queryKendra.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/56583580fbc767c70ca451a09cc98ce7c299b998/packages/cdk/lambda/queryKendra.ts)
      - タイムアウトは 15 分
      - AWS SDK を明示的にバンドル (`bundling: externalModules: []` を指定)
      - 環境変数の `INDEX_ID` に Kendra インデックス ID を指定
      - 処理概要
        - Kendra インデックスに対して[問合せコマンド](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/kendra/command/QueryCommand/)を実行
          - `IndexId`: 環境変数の `INDEX_ID`
          - `QueryText`: `event.body.query`
          - `AttributeFilter`: `_language_code: ja` を指定した [AttributeFilter](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/kendra/command/QueryCommand/)
          - 戻り値に実行結果を設定して終了
    - `Retrieve` Lambda 関数を作成する
      - ランタイムは NodeJS の最新版
      - ソースコードは [`packages/cdk/lambda/retrieveKendra.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/56583580fbc767c70ca451a09cc98ce7c299b998/packages/cdk/lambda/retrieveKendra.ts)
      - タイムアウトは 15 分
      - AWS SDK を明示的にバンドル (`bundling: externalModules: []` を指定)
      - 環境変数の `INDEX_ID` に Kendra インデックス ID を指定
      - 処理概要
        - Kendra インデックスに対して[取得コマンド](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/kendra/command/RetrieveCommand/)を実行
          - `IndexId`: 環境変数の `INDEX_ID`
          - `QueryText`: `event.body.query`
          - `AttributeFilter`: `_language_code: ja` を指定した [AttributeFilter](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/kendra/command/RetrieveCommand/)
          - 戻り値に実行結果を設定して終了
    - `Api` リソースの API エンドポイントに上記関数の呼び出しを追加する
      - API GW オーソライザに `Auth` リソースの Cognito ユーザプールを指定
      - `/rag/query` (POST) => `Query` Lambda 関数を呼び出し
      - `/rag/retrieve` (POST) => `Retrieve` Lambda 関数を呼び出し

これで Rag リソースの作成は完了です。
加えて、GenerativeAiUseCasesStack から[`allowDownloadFile関数`](https://qiita.com/siruko/items/5f249f11847f3829c18a#cdk-%E7%94%A8%E9%96%A2%E6%95%B0allowdownloadfile) を呼び出し、データソース (ドキュメント格納用の S3 バケット) 内のファイルの署名付き URL を作成できるようにしています。

```typescript: typescript:packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
      if (rag.dataSourceBucketName) {
        api.allowDownloadFile(rag.dataSourceBucketName);
      }
```

今回もかなり長くなってしまいました。
次回は GenU 内の `GenerativeAiUseCasesStack` スタックの `RagKnowledgeBase` リソースから解説したいと思います。

## (参考) 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)