はじめに
皆さん、こんにちは。
私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、AWS GenU およびそれに必要なデータ基盤の探求 (Snowflake, dbt, Iceberg, etc) に取り組む必要があると考えています。
本投稿では、GenU のバックエンドである CDK コードを詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。
前回までのおさらい
前回までで、以下が完了しました。
- ①AWS CDK のセットアップ
- ②AWS CDK の動作確認
- ③GenU の概要
- ④GenU CDK スタックの概要
- ⑤CloudFrontWafStack スタックの解説
- ⑥RagKnowledgeBaseStack スタックの解説
- ⑦WebSearchAgentStack スタックの解説
- ⑧GuardrailStack スタックの解説
- ⑨GenerativeAiUseCasesStack > Auth スタックの解説
- ⑩GenerativeAiUseCasesStack > Database, Api スタックの解説
GenU の CDK は最大で以下の 6 つの子スタックを作成します。
CloudFrontWafStack
RagKnowledgeBaseStack
AgentStack
GuardrailStack
-
GenerativeAiUseCasesStack
※メインスタック DashboardStack
第 9 回から GenU 内の本丸である GenerativeAiUseCasesStack
スタックを解説しています。
GenerativeAiUseCasesStack スタック
GenerativeAiUseCasesStack スタックでは、以下のリソースを作成しています。
Auth
Database
Api
CommonWebAcl
Web
Rag
RagKnowledgeBase
UseCaseBuilder
Transcribe
今回は CommonWebAcl
, Web
, Rag
リソースを解説していきます。
GenerativeAiUseCasesStack > CommonWebAcl (WAF) リソース
CommonWebAcl (WAF) リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。
以下のソースコードが CommonWebAcl の定義です。
// 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 リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。
以下のソースコードが Web の定義です。
// 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
にあります。
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 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 のパラメータ
Environment.ComputeType
をComputeType.MEDIUM
で上書き
GenerativeAiUseCasesStack > Rag リソース
Rag リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。
以下のソースコードが Rag の定義です。
// 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 を作成します。
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 マネージドキーでの暗号化
- スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
- オブジェクトの所有者: オブジェクトをアップロードしたアカウント
- 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
を許可ポリシーをアタッチ
- ドキュメント格納用 S3 バケットの
-
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 を起動停止するStepFunctions
とEventBridge Rule
を作成する- スタック作成時のパラメータ
kendraIndexScheduleCreateCron
(デフォルト値は null) に cron 値が設定されている場合-
StepFunctionsStateMachineKendraOn
StepFunctions ステートマシンを作成する- ステートマシンのタイムアウトは 3 時間 (180 分)
-
TaskUpdateCloudformationStackWithKendraOn
ジョブから開始- CloudFormation の
updateStack
を呼び出す - パラメータ
KendraState: on
を設定し Kendra インデックスを作成
- CloudFormation の
-
TaskCheckCloudformationState
ジョブに遷移- CloudFormation の
describeStacks
を呼び出す
- CloudFormation の
- CloudFormation の実行が完了するまで 5 分待機を繰り返す
-
TaskCallStateMachineStartDataSourceSyncJob
ジョブに遷移- Kendra データソース同期する
StepFunctionsStateMachineStartDataSourceSyncJob
StepFunctions ステートマシンを実行- ステートマシンのタイムアウトは 3 時間 (180 分)
-
TaskStartDataSourceSyncJob
ジョブから開始 (このジョブのみなので終了)- Kendra の
startDataSourceSyncJob
を呼び出す
- Kendra の
- Kendra データソース同期する
-
CronJobKendraOn
EventBridge Rule を作成する-
kendraIndexScheduleCreateCron
の値でスケジューリング - ターゲットに
StepFunctionsStateMachineKendraOn
StepFunctions ステートマシンを設定
-
-
- スタック作成時のパラメータ
kendraIndexScheduleDeleteCron
(デフォルト値は null) に cron 値が設定されている場合-
StepFunctionsStateMachineKendraOff
StepFunctions ステートマシンを作成する- ステートマシンのタイムアウトは 3 時間 (180 分)
-
TaskUpdateCloudformationStackWithKendraOff
ジョブから開始 (このジョブのみなので終了)- CloudFormation の
updateStack
を呼び出す - パラメータ
KendraState: off
を設定し Kendra インデックスを削除
- CloudFormation の
-
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 - 戻り値に実行結果を設定して終了
-
- Kendra インデックスに対して問合せコマンドを実行
-
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 - 戻り値に実行結果を設定して終了
-
- Kendra インデックスに対して取得コマンドを実行
-
Api
リソースの API エンドポイントに上記関数の呼び出しを追加する- API GW オーソライザに
Auth
リソースの Cognito ユーザプールを指定 -
/rag/query
(POST) =>Query
Lambda 関数を呼び出し -
/rag/retrieve
(POST) =>Retrieve
Lambda 関数を呼び出し
- API GW オーソライザに
-
-
これで Rag リソースの作成は完了です。
加えて、GenerativeAiUseCasesStack からallowDownloadFile関数
を呼び出し、データソース (ドキュメント格納用の S3 バケット) 内のファイルの署名付き URL を作成できるようにしています。
if (rag.dataSourceBucketName) {
api.allowDownloadFile(rag.dataSourceBucketName);
}
今回もかなり長くなってしまいました。
次回は GenU 内の GenerativeAiUseCasesStack
スタックの RagKnowledgeBase
リソースから解説したいと思います。
(参考) GenU のバックエンド (CDK) 詳細解説投稿一覧
- ①AWS CDK のセットアップ
- ②AWS CDK の動作確認
- ③GenU の概要
- ④GenU CDK スタックの概要
- ⑤CloudFrontWafStack スタックの解説
- ⑥RagKnowledgeBaseStack スタックの解説
- ⑦WebSearchAgentStack スタックの解説
- ⑧GuardrailStack スタックの解説
- ⑨GenerativeAiUseCasesStack > Auth スタックの解説
- ⑩GenerativeAiUseCasesStack > Database, Api スタックの解説
- ⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの解説
- ⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの解説
- ⑬DashBoard スタックの解説
- ⑭GenU の Outputs の解説