神戸のデータ活用塾!KDL Data Blog

KDLが誇るデータ活用のプロフェッショナル達が書き連ねるブログです。

AWSのBedrockとKendraを使ってRAGの仕組み作ってみた!~Terraform版~〔前編〕

データインテリジェンスチームの畑です。
今回のブログテーマは「AWSのBedrockとKendraを使ってRAGの仕組み作ってみた!~Terraform版~」です。以前のブログでMicrosoft Azure(以下Azure)のPromptflowを使ったRAG(※1)の作り方をご紹介しました。同じことをAmazon Web Services(以下AWS)でもやってみます。コンソール上での作成は色々なブログで紹介されているのでTerraformを使った作成に主眼をおいてご紹介します。AzureのPromptflowを使ったRAGの作り方についてはこちらの記事をご覧ください。
kdl-di.hatenablog.com ※1 RAG
Retrieval-Augmented Generation(検索拡張生成)の略で、データ検索と文章生成を組み合わせて情報提供精度を向上させる技術

Terraformとは

コードでインフラを管理できるIaCツール(Infrastructure as Code)の1つです。
HashiCorp社によって開発されており、コードでほぼ全ての設定を管理できるのがポイントです。

メリット

環境の作成・削除が容易
一度コードが完成すれば環境の作成、削除はコマンド1つで完了します。コンソール画面上でリソースを1つ1つ作成するのに比べると圧倒的に速いです。

変更履歴を追いやすい
コードで管理されているため、誰がいつどんな変更を加えたなど変更の履歴を追いやすくなっています。特に、複数人で開発する際に役に立つツールです

インフラ管理の学習コスト低減
各クラウドで専用のインフラ管理ツールはありますすが、その書き方が異なるため、クラウドごとに学習が必要です。一方、Terraformの場合は、Azure、AWS、GCPなど主要なクラウドサービスに対応しているため、インフラ管理の学習コストを減らすことができます。

デメリット

コード化に時間がかかる
コンソール画面上に比べるとイメージが湧きにくく、ボタンで設定もできないので完成までに時間がかかります。使いたいリソースをまずはコンソール上で触ってみてから、Terraformで作成するのがおすすめです。

Terraformのインストールと基本操作について

ドキュメントにインストール方法、基本的な概念や使い方が記載されているので参考にしてください。ここでは、必要最低限の基本操作の説明にとどめます。

### 基本コマンド
# 新規プロジェクトの作成・初期化
terraform init

# 実行計画の確認
terraform plan

# デプロイ
terraform apply

# 環境削除 
terraform destroy

今回作成するアーキテクチャー

Promptflowで構築したRAGと同様、KDLデータ活用塾のブログ最新10件のデータから質問回答するAPIを作成したいと思います。Bedrock+Kendraに加えてデータソースにはS3、API呼び出しの仕組みはLambdaとAPI-Gatewayで作成します。作成するアーキテクチャーは以下のとおりです。東京リージョン(ap-northeast-1)では機能の制限があるため、バージニア北部リージョン(us-east-1)でリソースを作成します。

Bedrockのモデル準備

続いてBedrockのモデルの準備をしていきます。今回は日本語も対応しているAnthropicのClaudeとClaudeInstantを使います。初期設定ではアクセスが不可になっていることが多いので、以下の手順で利用できるよう設定しましょう。

モデルへのアクセス権の取得

ClaudeとClaudeInstantの違いは性能とコストです。Claudeのほうがより複雑なタスクを処理でき、最大入力トークンも多いです。一方、ClaudeInstantはus-east-1やus-west-2であればコストがClaudeの1/10と低く抑えられます。まずはClaudeInstantで試してみて、思った回答が得られない場合にClaudeを使ってみるというのが良さそうです。

ClaudeとClaudeInstantのコスト比較

Terraformのコード

フォルダ構成

root
├─.terraform
│  └─・・・
├─main.tf
├─provider.tf
├─variable.tf
├─envs
│  └─terraform.tfvars
└─modules
    ├─api-gateway
    │  ├─main.tf
    │  ├─variables.tf
    │  └─outputs.tf
    ├─iam
    │  ├─main.tf
    │  ├─variables.tf
    │  └─outputs.tf
    ├─kendra
    │  ├─main.tf
    │  ├─variables.tf
    │  └─outputs.tf
    ├─lambda
    │  ├─main.tf
    │  ├─variables.tf
    │  ├─outputs.tf
    │  ├─src
    │  │  ├─・・・
    │  │  └─lambda.py
    │  └─upload
    │      └─lambda.zip  
    └─s3
       ├─main.tf
       ├─variables.tf
       └─outputs.tf
  • main.tf:各モジュールを呼び出すテンプレートファイル
  • provider.tf:Terraformやawsのプロバイダーの設定をするテンプレートファイル
  • variable.tf:変数を設定するテンプレートファイル
  • envs/Terraform.tfvars:変数定義用の外部ファイル
  • modules/<リソース>:AWSの各リソースをモジュール化して格納
  • modules/<リソース>/main.tf:AWSの各リソースを作成するテンプレートファイル
  • modules/<リソース>/variables.tf:AWSの各リソース作成に必要な変数を定義するテンプレートファイル
  • modules/<リソース>/outputs.tf:AWSの各リソース作成時に出力変数を定義するテンプレートファイル

コード内容

各モジュールのテンプレートファイル(.tf)

一番シンプルなS3のモジュールのテンプレートファイルを例に基本構造を説明します。
他のリソースについても基本の構造は同じですので、細かな説明は省きます。

resource "aws_s3_bucket" "main" {
  bucket = "${var.resource_prefix}-s3-bucket" 
}
variable "resource_prefix" {
  type = string
}
output "bucket_name" {
  value = aws_s3_bucket.main.bucket

ブロックについて
{ }で囲まれたひとまとまりがブロックです。ブロックには種類があり先頭のvariable、resource等です。{ }の前に何を記載するかはブロックの役割によって異なります。

  • variableブロック
    変数の定義をしています。"resource_prefix"が後述のmain.tfからの入力になっています。typeでデータ型を文字列に指定しています。

  • resourceブロック
    AWSのリソースを定義しています。resource <リソースタイプ> <リソース名> { }と定義します。"aws_s3_bucket"がS3のバケットを作成するリソースタイプです。リソースの名は"main"です。リソース名は自身で任意に設定します。{ }内の書き方はドキュメントを確認してください。ここではバケットの名前を定義しています。

  • outputブロック
    出力変数を定義します。"bucket_nameが出力変数名でvalueで出力値を定義しています。
# インデックスを作成する
resource "aws_kendra_index" "main" {
  name        = "${var.resource_prefix}-index"
  edition     = "DEVELOPER_EDITION"
  role_arn    = "${var.kendra_role_arn}"
  depends_on = [var.log_group_kendra]
}

# S3データソースを作成する
resource "aws_kendra_data_source" "main" {
  index_id = aws_kendra_index.main.id
  name     = "${var.resource_prefix}-data-source-s3"
  type     = "S3"
  role_arn = "${var.kendra_role_arn}"
  language_code = "ja"
  depends_on = [var.log_group_kendra]
  configuration {
    s3_configuration {
      bucket_name = "${var.bucket_name}"
    }
  }
}
variable "resource_prefix" {
  type = string
}
variable "kendra_role_arn" {
  type = string
}
variable "bucket_name" {
  type = string
}
variable "log_group_kendra" {
  type = string
}

output "index_id" {
  value = aws_kendra_index.main.id
}
output "kendra_arn" {
  value = aws_kendra_index.main.arn
}

# Lambda関数の実行に必要なファイルをzip化する
data "archive_file" "lambda" {
  depends_on = [null_resource.pip_install]
  type        = "zip"
  source_dir  = "${path.module}/src"
  output_path = "${path.module}/upload/lambda.zip"
}

# Lambda関数のリソースを作成する
resource "aws_lambda_function" "lambda" {
  filename         = data.archive_file.lambda.output_path
  function_name    = "${var.resource_prefix}_lambda"
  role             = var.lambda_role_arn
  handler          = "lambda.handler"
  source_code_hash = data.archive_file.lambda.output_base64sha256
  runtime          = "python3.8"
  timeout          = 60
  environment {
    variables = {
      BUCKET_NAME = var.bucket_name,
      INDEX_ID = var.index_id
    }
  }
  depends_on = [var.log_group_lambda]
}

# Lambda関数の実行権限を設定する
resource "aws_lambda_permission" "lambda_permit" {               
  statement_id  = "AllowAPIGatewayGetTrApi"                         
  action        = "lambda:InvokeFunction"                            
  function_name = aws_lambda_function.lambda.arn                 
  principal     = "apigateway.amazonaws.com"                        
  source_arn    = "${var.api-execution-arn}/*/POST/*"           
}                                                                   

# requirements.txtを元にLambda関数に必要なライブラリをインストールする
resource "null_resource" "pip_install" {
  triggers = {
    "requirements_diff" = filebase64("${path.module}/src/requirements.txt")
  }

  provisioner "local-exec" {
        command = "pip install -r ${path.module}/src/requirements.txt -t ${path.module}/src"
  }
}

# Lambda関数のレイヤーを作成する
resource "aws_lambda_layer_version" "lambda_layer" {
    filename = "${data.archive_file.lambda.output_path}"
    layer_name = "lambda"

    compatible_runtimes = ["python3.8"]
}
variable "resource_prefix" {
  type = string
}
variable "lambda_role_arn" {
  type = string
}
variable "log_group_lambda" {
  type = string
}
variable "api-execution-arn" {               
  type = string                                 
}
variable "bucket_name" {
  type = string
}
variable "index_id" {
  type = string
}
output "lambda-invoke-arn" {
  value = aws_lambda_function.lambda.invoke_arn
}

lambda関数の詳細については次回のブログで紹介します。

# API-Gatewayのリソース作成
resource "aws_api_gateway_rest_api" "api" {
  name = "${var.resource_prefix}-api"
}
API-Gatewayのメソッド作成
resource "aws_api_gateway_method" "api_post" {
  authorization = "NONE"
  http_method   = "POST"
  resource_id   = aws_api_gateway_rest_api.api.root_resource_id
  rest_api_id   = aws_api_gateway_rest_api.api.id
}

API-GatewayのLambda統合 resource "aws_api_gateway_integration" "api_post" { http_method = aws_api_gateway_method.api_post.http_method resource_id = aws_api_gateway_rest_api.api.root_resource_id rest_api_id = aws_api_gateway_rest_api.api.id integration_http_method = "POST" type = "AWS_PROXY" uri = var.lambda-invoke-arn }

API-Gatewayのデプロイ resource "aws_api_gateway_deployment" "api" { depends_on = [ aws_api_gateway_integration.api_post ] rest_api_id = aws_api_gateway_rest_api.api.id stage_name = "test" triggers = { redeployment = filebase64("${path.module}/main.tf") } }

variable "resource_prefix" {
  type = string
}
variable "lambda-invoke-arn" {
  type = string
}
output "api-execution-arn" {
  value = aws_api_gateway_rest_api.api.execution_arn
}
# リソース作成
##############
# Lambda
##############
# Lambda用のIAMロールの作成
resource "aws_iam_role" "lambda" {
  name = "${var.resource_prefix}-lambda"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

# Bedrock動作用のポリシー
resource "aws_iam_policy" "bedrock" {
  name        = "BedrockAccessPolicy"
  description = "Policy allowing Bedrock actions"
  
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "bedrock:*",
        "Resource": "*"
      }
    ]
  })
}
# Kendraのデータ読み取り用のポリシー
resource "aws_iam_policy" "lambda_to_kendra" {
  name        = "LambdaToKendraPolicy"
  description = "Policy allowing Kendra actions"
  
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "kendra:Retrieve",
        "Resource": "${var.kendra_arn}"
      }
    ]
  })
}
# CloudWatchログへの書き込み許可ポリシーをlambdaのロールにアタッチ
resource "aws_iam_role_policy_attachment" "lambda" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# # S3バージョン
# resource "aws_iam_role_policy_attachment" "lambda_s3_access" {
#   role       = aws_iam_role.lambda.name
#   policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"  # S3読み取り専用アクセス権限のARN
# }
# Bedrock用のポリシーをlambdaのロールにアタッチ
resource "aws_iam_role_policy_attachment" "bedrock" {
  role       = aws_iam_role.lambda.name
  policy_arn = aws_iam_policy.bedrock.arn
}
# Kendra用のポリシーをlambdaのロールにアタッチ
resource "aws_iam_role_policy_attachment" "lambda_to_kendra" {
  role       = aws_iam_role.lambda.name
  policy_arn = aws_iam_policy.lambda_to_kendra.arn
}

##############
# Kendra
##############
# kendra用のIAMロール
resource "aws_iam_role" "kendra_role" {
  name = "${var.resource_prefix}_kendra_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "kendra.amazonaws.com"
        }
      }
    ]
  })
}

# S3データソース用のポリシー
resource "aws_iam_policy" "kendra_policy" {
  name        = "${var.resource_prefix}-kendra-policy"
  description = "S3へのアクセスとKendraへのドキュメントの追加・削除権限"
  
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::${var.bucket_name}/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::${var.bucket_name}"
            ],
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kendra:BatchPutDocument",
                "kendra:BatchDeleteDocument"
            ],
            "Resource": "${var.kendra_arn}"
        }
    ]
})
}
# Amazon Kendra に CloudWatch ログへのアクセスを許可するロールポリシー
resource "aws_iam_policy" "cloudwatch_policy" {
  name        = "${var.resource_prefix}-cloudwatch-policy"
  description = "CloudWatchロググループへのアクセス権限"
  
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "cloudwatch:PutMetricData",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "cloudwatch:namespace": "AWS/Kendra"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": "logs:DescribeLogGroups",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/kendra/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:DescribeLogStreams",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/kendra/*:log-stream:*"
        }
    ]
})
}
# Kendraのロールにポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "kendra" {
  role       = aws_iam_role.kendra_role.name
  policy_arn = aws_iam_policy.kendra_policy.arn
}
# KendraのロールにCloudWatchロググループへのアクセス権限をアタッチ
resource "aws_iam_role_policy_attachment" "cloudwatch" {
  role       = aws_iam_role.kendra_role.name
  policy_arn = aws_iam_policy.cloudwatch_policy.arn
}

# CloudWatchロググループ作成
# lambda用
resource "aws_cloudwatch_log_group" "lambda" {
  name = "/aws/lambda/${var.resource_prefix}"
}

# Kendra用
resource "aws_cloudwatch_log_group" "kendra" {
  name = "/aws/kendra/${var.resource_prefix}"
}
variable "resource_prefix" {
  type = string
}
variable "bucket_name" {
  type = string
}
variable "index_id" {
  type = string
}
variable "kendra_arn" {
  type = string
}
variable "region" {
  type = string
}
variable "account_id" {
  type = string
}
# KendraのロールARN出力
output "kendra_role_arn" {
  value = aws_iam_role.kendra_role.arn
}

# lambdaのロールARN出力
output "lambda_role_arn" {
  value = aws_iam_role.lambda.arn
}

# lambdaのロググループ名出力
output "log_group_lambda" {
  description = "cloudwatch log group of lambda"
  value       = aws_cloudwatch_log_group.lambda.name
}

# Kendraのロググループ名出力
output "log_group_kendra" {
  description = "cloudwatch log group of kendra"
  value       = aws_cloudwatch_log_group.kendra.name
}

以下を作成しています。

  • Kendra用のロール
  • Lambda用のロール
  • Kendra用のCloudWatch
  • Lambda用のCloudWatch

KendraのIAMロールの作成についてはドキュメントを参照してください。S3以外のデータソースを使う場合に関しても細かく指定方法が記載されています。

各モジュールの呼び出しテンプレートファイル

module "lambda" {
  source            = "./modules/lambda"
  resource_prefix   = var.resource_prefix
  lambda_role_arn   = module.iam.lambda_role_arn
  api-execution-arn = module.api_gateway.api-execution-arn
  log_group_lambda  = module.iam.log_group_lambda
  bucket_name       = module.s3.bucket_name
  index_id          = module.kendra.index_id
}

module "api_gateway" { source = "./modules/api-gateway" resource_prefix = var.resource_prefix lambda-invoke-arn = module.lambda.lambda-invoke-arn }

module "iam" { source = "./modules/iam" resource_prefix = var.resource_prefix bucket_name = module.s3.bucket_name index_id = module.kendra.index_id kendra_arn = module.kendra.kendra_arn region = var.region account_id = var.account_id }

module "s3" { source = "./modules/s3" resource_prefix = var.resource_prefix }

module "kendra" { source = "./modules/kendra" resource_prefix = var.resource_prefix kendra_role_arn = module.iam.kendra_role_arn bucket_name = module.s3.bucket_name log_group_kendra = module.iam.log_group_kendra }

モジュールブロック
moduleブロックを定義して各リソースをモジュールとして呼び出しています。main.tfがルートモジュール、各リソースは子モジュールと呼ばれます。sourceには呼び出したいモジュールのパスを記載します。それ以外は全て変数です。

子モジュール間での変数の受け渡し
例としてS3バケット名をKendraに変数として渡したい場合を考えます。テンプレートファイルに書くことは以下のとおりです。

  • s3/outputs.tfファイル
    outputブロックでバケット名を出力する。
  • main.tf
    Kendraのmoduleブロックにバケット名を変数として記載する。
  • kendra/variables.tf
    variableブロックにバケット名を入力として記載する。

    詳細についてはドキュメントを確認してください。

    子モジュール間の変数引き渡し

変数の定義・設定用のファイル

region = "us-east-1"
account_id = "xxxxxxxxx" # アカウントIDをいれる
resource_prefix = "rag-test-dev"

変数の設定ファイルです。外部ファイルとして環境情報等を読み込めます。
実行するときはterraform plan -var-file=envs/terraform.tfvarsのように読み込めます。

variable "region"{
    type = string
}
variable "resource_prefix" {
    type = string
}
variable "account_id" {
    type = string
}

変数の定義ファイルです。terraform.tfvarsで設定した変数を各テンプレートファイルで使えるように定義しています。
データ型しか定義していませんが、外部ファイルに記載がなかった場合のdefault値なども設定ができます。

プロバイダーの設定用テンプレートファイル

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }
  required_version = ">= 1.2.0"
}
provider "aws" {
  region = "us-east-1"
}

Terraformのバージョン、クラウドのプロバイダーの指定を行います。
これでTerraformのコード準備が完了しました。
次回はLambda関数について紹介した後、実際にデプロイしてAPIから質問・回答させてみましょう。

この記事で生成AIに興味を持たれた方はお気軽にお問合せください。

畑加奈子

データインテリジェンスチーム所属
元製造メーカー勤務。製品の不良検知を担当したことがきっかけとなり、データサイエンスに興味を持ちKDLへ。クラウドを利用したデータ活用に関してのトピックを中心に発信していきます。