🥓

TerraformでBedrockフローを構築してみた

に公開

Bedrockエージェントを運用する際にどこまでがIaCの責務なのかが気になったので、やってみました

構成図

arch

フロー


以下コード

設定系

  • リージョンは、Pinecone都合でオハイオを指定する
  • PineConeは事前にアカウント作成しAPIキーを発行し、環境変数に
provider.tf
terraform {
  required_version = "1.12.1"
  required_providers {
    pinecone = {
      source = "pinecone-io/pinecone"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "6.5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variable "pinecone_api_key" {
  type = string
}

provider "pinecone" {
  api_key = var.pinecone_api_key
}

export NAMESPACE=

export TF_VAR_namespace=$NAMESPACE
export TF_VAR_aws_region=$AWS_REGION

# Pinecone API Key (required)
export PINECONE_API_KEY=
export TF_VAR_pinecone_api_key=$PINECONE_API_KEY

main

  • pineconeモジュールとawsモジュールに分割
  • pinecone⇒awsに渡す情報
    • apiキー : SecretManagerに保存する
    • 接続文字列: DBのアレをナレッジベースで設定する
main.tf
locals {
  # Pinnecone の都合でオハイオリージョン固定
  aws_region = "us-east-1"
}

module "pinecone" {
  source     = "./pinecone"
  aws_region = local.aws_region
}

module "aws" {
  source                  = "./aws"
  namespace               = var.namespace
  pinecone_api_key        = var.pinecone_api_key
  dense_connection_string = module.pinecone.dense_connection_string
  aws_region              = local.aws_region
}

pineconeモジュール

  • リージョンは、 us-east-1 を指定する
  • dimensionは、モデルに合わせて指定する
    • 埋め込みモデルとして利用する Amazon Titan v2 が1024のため
./pinecone/main.tf
variable "aws_region" {
  type = string
}

# https://registry.terraform.io/providers/pinecone-io/pinecone/latest/docs

resource "pinecone_index" "dense" {
  name      = "dense-index"
  dimension = 1024
  spec = {
    serverless = {
      cloud  = "aws"
      region = "us-east-1"
    }
  }
}

output "dense_connection_string" {
  value = pinecone_index.dense.host

}

aws モジュール

  • SecretManagerとBedrockが対象
  • AWSサービスごとにモジュール化
  • 依存関係
    • pinecone APIキー & 接続文字列
      • APIキー⇒AWS SecretManager
        • SecretManager ARN ⇒ Bedrock
      • 接続文字列⇒Bedrock
./aws/main.tf

module "secret_manager" {
  source = "./modules/secret-manager"

  namespace        = var.namespace
  pinecone_api_key = var.pinecone_api_key
}

module "bedrock" {
  source = "./modules/bedrock"

  namespace               = var.namespace
  pinecone_api_key        = var.pinecone_api_key
  dense_connection_string = var.dense_connection_string
  secret_manager_arn      = module.secret_manager.secret_manager_arn

  aws_region = var.aws_region
  account_id = data.aws_caller_identity.current.account_id
}

SecretManager

  • シークレット名の接頭辞に AmazonBedrock- を付けないといけない (2hハマった)
  • PineconeのAPIキーを暗号化し保存する
  • jsonencode({ apiKey = var.pinecone_api_key}) で保存する
  • シークレット名の接頭辞に AmazonBedrock- を付けないといけない
./aws/modules/secret-manager.tf
resource "aws_secretsmanager_secret" "pinecone_api_key" {
  name        = "AmazonBedrock-${var.namespace}-pinecone-api-key"
  description = "Pinecone API Key for Bedrock Knowledge Base"
}

resource "aws_secretsmanager_secret_version" "pinecone_api_key" {
  secret_id = aws_secretsmanager_secret.pinecone_api_key.id
  secret_string = jsonencode({
    apiKey = var.pinecone_api_key
  })
}

Bedrock

ナレッジベース

  • ナレッジベース本体、データソース(S3)の作成
  • ナレッジベース本体で埋め込みモデルの指定とPineCone連携を設定する
  • apply し終わったら、マネコンからナレッジベースを開き「同期」を押すとPineConeにS3のデータが登録される

manecon

./aws/modules/bedrock/knowledge-base.tf
resource "aws_bedrockagent_knowledge_base" "bedrock_knowledge_base_v2" {
  description = null
  name        = "${var.namespace}-knowledge-base-v2"
  role_arn    = aws_iam_role.knowledge_base_role.arn
  tags        = null
  knowledge_base_configuration {
    type = "VECTOR"
    vector_knowledge_base_configuration {
      embedding_model_arn = "arn:aws:bedrock:${var.aws_region}::foundation-model/amazon.titan-embed-text-v2:0"
      embedding_model_configuration {
        bedrock_embedding_model_configuration {
          dimensions          = 1024
          embedding_data_type = "FLOAT32"
        }
      }
    }
  }
  storage_configuration {
    type = "PINECONE"
    pinecone_configuration {
      connection_string      = "https://${var.dense_connection_string}"
      credentials_secret_arn = var.secret_manager_arn
      namespace              = null
      field_mapping {
        metadata_field = "metadata"
        text_field     = "text"
      }
    }
  }
}


resource "aws_s3_bucket" "name" {
  bucket = "${var.namespace}-rag-bucket-v2"
}

resource "aws_bedrockagent_data_source" "rag_data_source" {
  name              = "${var.namespace}-rag-data-source-v2"
  knowledge_base_id = aws_bedrockagent_knowledge_base.bedrock_knowledge_base_v2.id

  data_source_configuration {
    type = "S3"
    s3_configuration {
      bucket_arn = aws_s3_bucket.name.arn
    }
  }

  depends_on = [aws_bedrockagent_knowledge_base.bedrock_knowledge_base_v2, aws_s3_bucket.name]
}

prompt

  • テキストというより、LLMに渡すパラメタを一括りで「プロンプト」と呼んでいる
  • LLMへの指示 + top p などの指定
./aws/modules/bedrock/prompt.tf
resource "aws_bedrockagent_prompt" "prompt" {
  default_variant             = "variantOne"
  name                        = "${var.namespace}-test"
  variant {
    additional_model_request_fields = null
    model_id                        = local.llm_model
    name                            = "variantOne"
    template_type                   = "TEXT"
    inference_configuration {
      text {
        max_tokens     = 512
        stop_sequences = ""
        temperature    = 0.699999988079071
        top_p          = 0.8999999761581421
      }
    }
    template_configuration {
      text {
        text = "This is my first text prompt. Please summarize on {{topic}}."
        input_variable {
          name = "topic"
        }
      }
    }
  }
}

flow

  • フローは手書きでの実装はむずかしいので、GUIで作成⇒importがおすすめ
  • 基本的にはIaCでの管理は不要の認識

./aws/modules/bedrock/flow.tf
resource "aws_bedrockagent_flow" "flow" {
  execution_role_arn          = aws_iam_role.agent_role.arn
  name                        = "${var.namespace}-ai-agent-flow"

  definition {
    connection {
      name   = "FlowInputNodeFlowInputNode0ToKnowledgeBaseNode_1KnowledgeBaseNode0"
      source = "FlowInputNode"
      target = "KnowledgeBaseNode_1"
      type   = "Data"
      configuration {
        data {
          source_output = "document"
          target_input  = "retrievalQuery"
        }
      }
    }
    connection {
      name   = "KnowledgeBaseNode_1KnowledgeBaseNode0ToPrompt_1PromptsNode0"
      source = "KnowledgeBaseNode_1"
      target = "Prompt_1"
      type   = "Data"
      configuration {
        data {
          source_output = "retrievalResults"
          target_input  = "topic"
        }
      }
    }
    connection {
      name   = "Prompt_1PromptsNode0ToFlowOutputNodeFlowOutputNode0"
      source = "Prompt_1"
      target = "FlowOutputNode"
      type   = "Data"
      configuration {
        data {
          source_output = "modelCompletion"
          target_input  = "document"
        }
      }
    }
    node {
      name = "FlowInputNode"
      type = "Input"
      configuration {
        input {
        }
      }
      output {
        name = "document"
        type = "String"
      }
    }
    node {
      name = "FlowOutputNode"
      type = "Output"
      configuration {
        output {
        }
      }
      input {
        category   = null
        expression = "$.data"
        name       = "document"
        type       = "String"
      }
    }
    node {
      name = "KnowledgeBaseNode_1"
      type = "KnowledgeBase"
      configuration {
        knowledge_base {
          knowledge_base_id = aws_bedrockagent_knowledge_base.bedrock_knowledge_base_v2.id
          model_id          = null
          number_of_results = null
        }
      }
      input {
        category   = null
        expression = "$.data"
        name       = "retrievalQuery"
        type       = "String"
      }
      output {
        name = "retrievalResults"
        type = "Array"
      }
    }
    node {
      name = "Prompt_1"
      type = "Prompt"
      configuration {
        prompt {
          source_configuration {
            inline {
              additional_model_request_fields = null
              model_id                        = local.llm_model
              template_type                   = "TEXT"
              inference_configuration {
                text {
                  max_tokens     = 512
                  stop_sequences = []
                  temperature    = 0.699999988079071
                  top_p          = 0.8999999761581421
                }
              }
              template_configuration {
                text {
                  text = "あなたは著名なポエマーです。受け取った文章をポエミーにしてください。\n {{topic}}."
                  input_variable {
                    name = "topic"
                  }
                }
              }
            }
          }
        }
      }
      input {
        category   = null
        expression = "$.data"
        name       = "topic"
        type       = "Array"
      }
      output {
        name = "modelCompletion"
        type = "String"
      }
    }
  }
}

まとめ

  • 変更頻度の低いイミュータブルリソースはTFで管理すると便利そう
  • 反対に変更頻度の高いリソース & ローコードであるフローをTFで管理する必要はない
  • 変更頻度の低いリソース
    • PineConeDB
    • SecretManager
    • ナレッジベース関連
    • IAMロール & ポリシー
  • 変更頻度の高いリソース
    • PineConeとS3に配置するデータ
    • プロンプト
  • TFで管理することでローコードの強みを打ち消してまうリソース
    • Bedrockフロー

所感

サービス横断でも対応できるTerraformイイね!

Discussion