0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-25

はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、AWS GenU およびそれに必要なデータ基盤の探求 (Snowflake, dbt, Iceberg, etc) に取り組む必要があると考えています。

本投稿では、GenU のバックエンドである CDK コードを詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。

前回までのおさらい

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

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

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

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

GenerativeAiUseCasesStack スタック

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

image.png

  • Auth
  • Database
  • Api
  • CommonWebAcl
  • Web
  • Rag
  • RagKnowledgeBase
  • UseCaseBuilder
  • Transcribe

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

GenerativeAiUseCasesStack > CommonWebAcl (WAF) リソース

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

image.png

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

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 にあります。

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

CommonWebAcl のコードはCloudFrontWafStack スタックですでに解説済 ですが、CloudFrontWafStack スタックとの違いは、scope: 'CLOUDFRONT' なのか scope: 'REGIONAL' なのかの違いになります。

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

GenerativeAiUseCasesStack > Web リソース

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

image.png

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

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 にあります。

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
    • CloudFrontWafStack で生成した WebACL があれば Cloudfront に設定する
  • 以下をすべて満たす場合のみ、独自ドメインアクセス用の ARecord を作成
    • 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 BuildNodejsBuild を用いて 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
      • スタック作成時のパラメータ agentsdisplayName (デフォルト値は空配列)
    • VITE_APP_INLINE_AGENTS: スタック作成時のパラメータ inlineAgents (デフォルト値は false)
    • VITE_APP_USE_CASE_BUILDER_ENABLED: スタック作成時のパラメータ useCaseBuilderEnabled (デフォルト値は true)
    • VITE_APP_HIDDEN_USE_CASES: スタック作成時のパラメータ hiddenUseCases (デフォルト値は空オブジェクト)
  • CodeBuld Project のパラメータ Environment.ComputeTypeComputeType.MEDIUM で上書き

GenerativeAiUseCasesStack > Rag リソース

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

image.png

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

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 にあります。
スタック作成時のパラメータ ragEnabled (デフォルト値は false) が true の場合、以下のコードを実行し RAG を作成します。

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 を作成
      • KendraIndexRoles3:GetObject の許可ポリシーをアタッチ
      • KendraIndexRoleCloudWatchLogsFullAccess のポリシーをアタッチ
    • アクセスログ保管用の DataSourceAccessLogsBucket S3 バケットを作成
      • ブロックパブリックアクセスはブロック
      • S3 マネージドキーでの暗号化
      • スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
      • オブジェクトの所有者: オブジェクトをアップロードしたアカウント
      • SSL 接続を強制する
    • ドキュメント格納用の DataSourceBucket S3 バケットを作成
      • ブロックパブリックアクセスはブロック
      • S3 マネージドキーでの暗号化
      • スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
      • オブジェクトの所有者: オブジェクトをアップロードしたアカウント
      • アクセスログ: アクセスログ保管用バケットの AccessLogs/ 配下を指定
      • SSL 接続を強制する
    • ドキュメント格納用 S3 バケットに、./rag-docs 配下のローカルファイルをアップロード
      • 誤ってログ用のフォルダにファイルを送信しないように除外フォルダを設定
    • Kendra インデックスの設定値 (kendra.CfnIndexProps)を作成する
      • name: generative-ai-use-cases-index[環境名]
      • edition: 固定値 'DEVELOPER_EDITION'
      • roleArn: KendraIndexRole の ARN
      • userContextPolicy: 固定値 'USER_TOKEN'
      • 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)を作成する
      • 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 を起動停止する StepFunctionsEventBridge Rule を作成する
      • スタック作成時のパラメータ 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
        • タイムアウトは 15 分
        • AWS SDK を明示的にバンドル (bundling: externalModules: [] を指定)
        • 環境変数の INDEX_ID に Kendra インデックス ID を指定
        • 処理概要
          • Kendra インデックスに対して問合せコマンドを実行
            • IndexId: 環境変数の INDEX_ID
            • QueryText: event.body.query
            • AttributeFilter: _language_code: ja を指定した AttributeFilter
            • 戻り値に実行結果を設定して終了
      • Retrieve Lambda 関数を作成する
        • ランタイムは NodeJS の最新版
        • ソースコードは packages/cdk/lambda/retrieveKendra.ts
        • タイムアウトは 15 分
        • AWS SDK を明示的にバンドル (bundling: externalModules: [] を指定)
        • 環境変数の INDEX_ID に Kendra インデックス ID を指定
        • 処理概要
          • Kendra インデックスに対して取得コマンドを実行
            • IndexId: 環境変数の INDEX_ID
            • QueryText: event.body.query
            • AttributeFilter: _language_code: ja を指定した AttributeFilter
            • 戻り値に実行結果を設定して終了
      • Api リソースの API エンドポイントに上記関数の呼び出しを追加する
        • API GW オーソライザに Auth リソースの Cognito ユーザプールを指定
        • /rag/query (POST) => Query Lambda 関数を呼び出し
        • /rag/retrieve (POST) => Retrieve Lambda 関数を呼び出し

これで Rag リソースの作成は完了です。
加えて、GenerativeAiUseCasesStack からallowDownloadFile関数 を呼び出し、データソース (ドキュメント格納用の S3 バケット) 内のファイルの署名付き URL を作成できるようにしています。

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?