データインテリジェンスチームの畑です。
今回のブログテーマは「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を使ってみるというのが良さそうです。
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へ。クラウドを利用したデータ活用に関してのトピックを中心に発信していきます。