RAGでよく使うKendraとS3をCloudFormationで実装してみた

2023.11.03

こんにちは、つくぼし(tsukuboshi0755)です!

最近流行りのRetrieval-Augmented Generation(以下RAG)を組む際に、文書を保存するデータソースとしてKendraとS3を組み合わせるパターンは多いと思います。

今回は、RAGでよく使うKendraとS3をCloudFormationで実装し、利用する方法を紹介します!

構成

今回の構成図は以下になります。

また以下の設定はRAGには必須ではありませんが、実際にRAGを運用する際に役に立つので合わせて設定します。

  • CloudWatch Logsの使用料金を減らすため、Kendraログに保持期間を指定
  • S3バケットに保存された文書データが不正にダウンロードされる事を防ぐため、バケットポリシーでKendraデータソースロール以外からのオブジェクト取得を禁止

テンプレート

以下のリポジトリのテンプレートをベースにしています。

amazon-kendra-langchain-extensions/kendra_retriever_samples/kendra-docs-index.yaml

リポジトリのテンプレートはデータソースとしてWebクローラーを使用していますが、今回はS3を使用する形に変更した上で作成しています。

全体のコードは以下の通りです。

CloudFormationコード

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: Kendra and S3

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Parameters:
          - SysName
          - Env
          - KendraEdition
          - KendraLogRetentionDays
          - KendraDSBucketPrefix

Parameters:
  SysName:
    Type: String
    Default: 'cm'
    Description: 'System name for this stack.'
  Env:
    Type: String
    Default: 'prd'
    Description: 'Environment for this stack.'
    AllowedValues:
      - 'prd'
      - 'stg'
      - 'dev'
  KendraEdition:
    Type: String
    Default: 'ENTERPRISE_EDITION'
    Description: 'ENTERPRISE_EDITION is suitable for production environments. DEVELOPER_EDITION is suitable for development environments.'
    AllowedValues:
      - 'ENTERPRISE_EDITION'
      - 'DEVELOPER_EDITION'
  KendraLogRetentionDays:
    Type: Number
    Default: 365
    Description: 'Retention days for Kendra logs.'
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1096
      - 1827
      - 2192
      - 2557
      - 2922
      - 3288
      - 3653
  KendraDSBucketPrefix:
    Type: String
    Default: 'awsdoc'
    Description: 'Bucket prefix for search range.'


Resources:
  ##Role for Kendra Index
  KendraIndexRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${SysName}-${Env}-kendra-index-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: kendra.amazonaws.com
            Action: 'sts:AssumeRole'

  ##Policy for Kendra Index
  KendraIndexPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: !Sub '${SysName}-${Env}-kendra-index-policy'
      Roles:
        - !Ref KendraIndexRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Resource: '*'
            Condition:
              StringEquals:
                'cloudwatch:namespace': 'AWS/Kendra'
            Action:
              - 'cloudwatch:PutMetricData'
          - Effect: Allow
            Resource: '*'
            Action: 'logs:DescribeLogGroups'
          - Effect: Allow
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kendra/*'
            Action: 'logs:CreateLogGroup'
          - Effect: Allow
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kendra/*:log-stream:*'
            Action:
              - 'logs:DescribeLogStreams'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'

  ##Kendra Index
  KendraIndex:
    Type: 'AWS::Kendra::Index'
    Properties:
      Name: !Sub '${SysName}-${Env}-kendra-index'
      Edition: !Ref KendraEdition
      RoleArn: !GetAtt KendraIndexRole.Arn

  ##CloudWatch LogGroup for Kendra Index
  KendraIndexLogs:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub
        - '/aws/kendra/${IndexId}'
        -  IndexId: !GetAtt KendraIndex.Id
      RetentionInDays: !Ref KendraLogRetentionDays

  ##Role for Kendra Data Source
  KendraDSRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${SysName}-${Env}-kendra-ds-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: kendra.amazonaws.com
            Action: 'sts:AssumeRole'

  ##Policy for Kendra Data Source
  KendraDSPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: !Sub '${SysName}-${Env}-kendra-ds-policy'
      Roles:
        - !Ref KendraDSRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref KendraDSBucket
                  - '/*'
            Action:
              - 's3:GetObject'
          - Effect: Allow
            Resource: !GetAtt KendraDSBucket.Arn
            Action:
              - 's3:ListBucket'
          - Effect: Allow
            Resource: !Sub
              - 'arn:aws:kendra:${AWS::Region}:${AWS::AccountId}:index/${IndexId}'
              -  IndexId: !GetAtt KendraIndex.Id
            Action:
              - 'kendra:BatchPutDocument'
              - 'kendra:BatchDeleteDocument'

  ##Kendra Data Source
  KendraDS:
    Type: 'AWS::Kendra::DataSource'
    Properties:
      DataSourceConfiguration:
        S3Configuration:
          BucketName: !Ref KendraDSBucket
          InclusionPrefixes:
            - !Ref KendraDSBucketPrefix
      IndexId: !GetAtt KendraIndex.Id
      LanguageCode: 'ja'
      Name: !Sub '${SysName}-${Env}-kendra-ds'
      RoleArn: !GetAtt KendraDSRole.Arn
      Type: 'S3'

  ##Data Source Bucket
  KendraDSBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${SysName}-${Env}-kendra-ds-bucket-${AWS::AccountId}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
            BucketKeyEnabled: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced

  ##Data Source Bucket Policy
  KendraDSBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref KendraDSBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Deny
            Action:
              - 's3:GetObject'
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref KendraDSBucket
                  - '/*'
            Principal: '*'
            Condition:
                StringNotEquals:
                  aws:PrincipalArn:
                    - !GetAtt KendraDSRole.Arn

Outputs:
  KendraIndexID:
    Value: !GetAtt KendraIndex.Id
  KendraDSID:
    Value: !GetAtt KendraDS.Id
  KendraDSBucketName:
    Value: !Ref KendraDSBucket

以下より、本テンプレートで作成される各リソースについて説明します。

Kendraインデックスロール

  KendraIndexRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${SysName}-${Env}-kendra-index-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: kendra.amazonaws.com
            Action: 'sts:AssumeRole'

  KendraIndexPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: !Sub '${SysName}-${Env}-kendra-index-policy'
      Roles:
        - !Ref KendraIndexRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Resource: '*'
            Condition:
              StringEquals:
                'cloudwatch:namespace': 'AWS/Kendra'
            Action:
              - 'cloudwatch:PutMetricData'
          - Effect: Allow
            Resource: '*'
            Action: 'logs:DescribeLogGroups'
          - Effect: Allow
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kendra/*'
            Action: 'logs:CreateLogGroup'
          - Effect: Allow
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kendra/*:log-stream:*'
            Action:
              - 'logs:DescribeLogStreams'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'

Kendraインデックス用のIAMロールを作成します。

ここでは以下の公式ドキュメントに基づき、Kendraに関するCloudWatchログ及びメトリクスの出力権限をアタッチします。

Kendraインデックス

  KendraIndex:
    Type: 'AWS::Kendra::Index'
    Properties:
      Name: !Sub '${SysName}-${Env}-kendra-index'
      Edition: !Ref KendraEdition
      RoleArn: !GetAtt KendraIndexRole.Arn

文書データを検索するKendraインデックスを作成します。

Editionでは、KendraのエディションをENTERPRISE_EDITION(本番用)またはDEVELOPER_EDITION(検証用)から選択します。

RoleArnでは、先ほど作成したKendraインデックスロールARNを指定します。

Kendraログ(任意)

  KendraIndexLogs:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub
        - '/aws/kendra/${IndexId}'
        -  IndexId: !GetAtt KendraIndex.Id
      RetentionInDays: !Ref KendraLogRetentionDays

Kendra用のCloudWatch Logsロググループを作成します。

必要に応じて、RetentionInDaysでKendraログの保持期間を指定します。

Kendraデータソースロール

  KendraDSRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${SysName}-${Env}-kendra-ds-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: kendra.amazonaws.com
            Action: 'sts:AssumeRole'

  KendraDSPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: !Sub '${SysName}-${Env}-kendra-ds-policy'
      Roles:
        - !Ref KendraDSRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref KendraDSBucket
                  - '/*'
            Action:
              - 's3:GetObject'
          - Effect: Allow
            Resource: !GetAtt KendraDSBucket.Arn
            Action:
              - 's3:ListBucket'
          - Effect: Allow
            Resource: !Sub
              - 'arn:aws:kendra:${AWS::Region}:${AWS::AccountId}:index/${IndexId}'
              -  IndexId: !GetAtt KendraIndex.Id
            Action:
              - 'kendra:BatchPutDocument'
              - 'kendra:BatchDeleteDocument'

Kendraデータソース用のIAMロールを作成します。

ここではS3上の文書データへのアクセス権限、及びKendraインデックスへの文書データの登録・削除権限をアタッチします。

Kendraデータソース

  KendraDS:
    Type: 'AWS::Kendra::DataSource'
    Properties:
      DataSourceConfiguration:
        S3Configuration:
          BucketName: !Ref KendraDSBucket
          InclusionPrefixes:
            - !Ref KendraDSBucketPrefix
      IndexId: !GetAtt KendraIndex.Id
      LanguageCode: 'ja'
      Name: !Sub '${SysName}-${Env}-kendra-ds'
      RoleArn: !GetAtt KendraDSRole.Arn
      Type: 'S3'

S3バケットの特定プレフィックス配下の文書データを対象とするKendraデータソースを作成します。

S3Configurationでは、文書データが保存されているバケット名とプレフィックスを指定します。

IndexIdでは、先ほど作成したKendraインデックスのIDを指定します。

LanguageCodeでは、文書データの言語コードを日本語(ja)で指定します。

RoleArnでは、先ほど作成したKendraデータソースロールARNを指定します。

Typeでは、文書データの保存先がS3バケットである事を指定します。

S3バケット

  KendraDSBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${SysName}-${Env}-kendra-ds-bucket-${AWS::AccountId}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
            BucketKeyEnabled: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced

文書データを保存するS3バケットを作成します。

今回は基本的なパブリックアクセス/暗号化/ACL無効化を実施しています。

S3バケットポリシー(任意)

  KendraDSBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref KendraDSBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Deny
            Action:
              - 's3:GetObject'
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref KendraDSBucket
                  - '/*'
            Principal: '*'
            Condition:
                StringNotEquals:
                  aws:PrincipalArn:
                    - !GetAtt KendraDSRole.Arn

Kendraデータソースロール以外からのオブジェクト取得を禁止するバケットポリシーを作成します。

このバケットポリシーを設定するで、S3バケットにアップロードした文書データに機密情報が含まれる場合でも、Kendraデータソース以外からの不正アクセスを防ぐ事ができます。

なおもしオブジェクトにアクセス可能なIAMロールを追加したい場合は、以下のような形で該当リソースのARNを追加してください。

            Condition:
                StringNotEquals:
                  aws:PrincipalArn:
                    - !GetAtt KendraDSRole.Arn
                    - !Sub "arn:aws:iam::${AWS::AccountId}:role/<IAMロール名>"
  • 参考

検索の確認

上記のテンプレートをデプロイする事で、Kendraを用いて正常に文書データを検索できるか確認します。

なお今回の確認で使用する文書データのアップロード及び確認手順としては、以下の記事を参照します。

CloudFormationスタックのデプロイ

今回は以下のパラメータで、CloudFormationスタックをデプロイします。

なおKendra関連リソースのデプロイには、およそ20-30分程度かかります。

S3バケットへのデータ投入

次にCloudShell上で以下のコマンドを実施し、PDFファイルを作成したS3バケットにアップロードします。

# S3 バケット名を設定
BUCKET_NAME=<バケット名>

# S3 プレフィックスを設定
BUCKET_PREFIX=awsdoc

# AWS の公式ドキュメントの PDF ファイルをダウンロード
mkdir ${BUCKET_PREFIX}
cd ${BUCKET_PREFIX}
wget https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/dynamodb-dg.pdf -O DynamoDB.pdf
wget https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-dg.pdf -O Lambda.pdf
wget https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-ug.pdf -O VPC.pdf
wget https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/kendra-dg.pdf -O Kendra.pdf
wget https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/route53-dg.pdf -O Route53.pdf
cd ..

# S3 バケットに PDF ファイルをアップロード
aws s3 cp ${BUCKET_PREFIX} s3://${BUCKET_NAME}/${BUCKET_PREFIX}/ --recursive

ファイルのアップロードが成功すると、以下の通り作成したS3バケットのawsdocプレフィックス配下に5つのPDFファイルが作成されます。

Kendraデータソースの同期

続いてKendraのコンソールに移動し、左ペインのIndexesに切り替え、作成したインデックスをクリックします。

Kendraのインデックス設定は以下のようになっています。

左ペインのData Sourcesに切り替え、作成したデータソースをクリックします。

Sync nowボタンを押すと、S3にアップロードしたPDFファイルがKendraに同期されます。

データの同期が成功すると、Sync Historyタブに以下のようなCompleted履歴が表示されます。

Kendraでの検索実行

Kendraで検索が可能か確認するために、左ペインのSearch indexed contentに切り替えます。

右端にある設定ボタンをクリックします。

Default language of source documentsでJapanese(ja)を選択し、saveボタンを押します。

これで検索設定は完了です。

試しに「IP アドレス」と検索すると、「IP」と「アドレス」の両方のキーワードが含まれているドキュメントがヒットしました!

なおもしバケットポリシーでKendraデータソースロール以外のオブジェクト取得を禁止している場合、ドキュメントに表示されているURLをクリックしてもアクセスできない事にご注意ください。

最後に

今回は、RAGでよく使うKendraとS3をCloudFormationで実装し、利用する方法を紹介しました!

今後RAGをAWSで組む機会は増えていくと考えられるので、その際にぜひご活用頂けるとありがたいです。

以上、つくぼし(tsukuboshi0755)でした!