Agents for Amazon Bedrock の IaC に挑戦！

# はじめに
こんにちは！**yu-Matsu**です！  

　ここ最近 Amazon Bedrock（以降、Bedrock） に関して検証していく過程で、個人アカウントにBedrock周りのリソースが乱立し始めています。あの時の検証の設定はどうなってたっけ？となることも多く、そろそろ整理を始ないとなぁと思っている今日この頃です。
　そこで、今回はその整理の一環として、**Terraform による Agents for Amazon Bedrock(以降、Agent) のコード化**を行い、検証内容をコード管理したいと思います。既に以下のように先駆者の方々が記事を残されていますので、それらを参考に進めたいと思います！

https://dev.classmethod.jp/articles/create-knowledge-bases-for-amazon-bedrock-via-terraform

https://dev.to/aws-builders/how-to-manage-an-amazon-bedrock-agent-using-terraform-1lag

# コード化したいAgent
　今回コード化したいAgentは、以前記事にしたことがある**ポケモン図鑑LINE Bot用のAgent**になります。

 https://qiita.com/yu-Matsu/items/5f26eb630620ec42342e

　概要は以下の通りです
- 図鑑番号を指定して質問すると、対応するポケモンの情報を答えてくれる
    **質問例 ）**「図鑑番号1000のポケモンについて教えて」
    　　
- ポケモンについて掘り下げた質問（対戦環境など）をすると、ドキュメントを検索して回答してくれる
  **質問例 ）**「〇〇を特殊アタッカーで運用する場合、性格は何がおすすめ？」

▼ 今回はLINE Botに乗せませんが、以下のようなイメージになります
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/71b49436-0781-5231-c303-01f3a92d04c3.png" width=70%>  
  
　また、Terraformのコード化に関して、ディレクトリ構成は以下のようにしました。今回はAgentを一つお試しでコード化するため、特にmodule構成等にはしておらず、雑なところもありますがご了承下さい。
```
terraform
  |- src
  |   |- poke_api
  |       |- index.py      # action用Lambdaのコード
  |
  |- provider.tf           # Terraformのproviderに関する設定
  |- backend.tf            # tfstateファイルの置き場所の設定
  |- datasource.tf         # datasourceに関する記述をまとめたファイル
  |- bedrock_agent.tf      # agentの定義に関するファイル
  |- bedrock_kb.tf         # knowledge base の定義に関するファイル
  |- lambda.tf             # lambdaの定義に関するファイル
  |- iam.tf                # iamの定義に関してまとめたファイル
```

# 事前準備
### Pinecone
　今回はベクトルデータベースとして**Pinecone**を利用したいと思いますので、先に準備をしておきます。

https://www.pinecone.io/

1. アカウントを作成していない場合はサインアップを実施する。（無料枠の場合は１アカウントにつき１データベースであることに注意）
2. サインアップ後以下のような画面に遷移するので、**Create index**を押下
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/e1323649-ee7c-4ae6-56aa-9fe7c5d2ef64.png" width=60%>　  
3. 必要な情報を入力し、indexを作成。Dimensionは、Embeddingのモデルに**Cohere Embed Multilingual**を利用するため、**1024**にする。それ以外の設定はそのままで大丈夫です
 <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/894f8d0d-52ea-f7ee-1aac-547936b1872f.png" width=60%>　
4. 作成が完了したら、indexの情報が表示される。後で必要になるため、**HOST**の情報は控えておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/ae6ab28f-c400-3e1c-da44-cc7c58e25ee0.png" width=60%>　
5. 最後に、左メニューから「API Key」を開き、デフォルトで作成されているAPIキーを確認して控えておきます。　
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/23d1a791-6494-79b2-dbdf-0e259f094420.png" width=60%>

### Knowledge baseのデータソース
　Knowledge baseのデータソースとなるドキュメントとして、今回は「**カイリュー**」
 「**サーフゴー**」、「**パオジアン**」という3匹のポケモンの対戦考察まとめwikiのページをPDF化したものを利用したいと思います。

https://latest.pokewiki.net/

PDFファイルを配置するためのS3バケットを事前に作成しておき、ファイルをアップロードしておきます。このS3バケットも手動で作成しました。

### Action Groups用Lambdaのソースコード
　Actions GroupsはAgentが実行出来るツール群のようなもので、Lambdaで定義することが出来ますので、先にLambdaのソースコードを準備しておきます。詳細は割愛しますが、前述した「**図鑑番号を指定して質問すると、対応するポケモンの情報を答えてくれる**」機能を実現するために、[PokeAPI](https://pokeapi.co/)というAPIを利用しています。
 
https://pokeapi.co/
```Python: src/poke_api/index.py
import json
import requests

BASE_URL="https://pokeapi.co/api/v2/"

def translate_info(info, value):
    url = "{base_url}{info}/{value}".format(base_url=BASE_URL, info=info, value=value)
    response = requests.get(url)
    
    if response.ok:
        data = response.json()
        
        for item in data['names']:
            if item['language']['name'] == 'ja-Hrkt':
                return item['name']
        return info
    else:
        return info
    
    
def lambda_handler(event, context):
    api_path = event["apiPath"]
    pokemon_id = event["parameters"][0]["value"]
    
    endpoint = BASE_URL + "pokemon/"
    
    url = endpoint + pokemon_id
    
    if api_path == "/search":
        response = requests.get(url)
        
        response = response.json()
        print(response)
        
        # タイプが複数ある場合の処理
        formatted_types = [ translate_info("type", poke_type["type"]["name"]) for poke_type in response["types"] ]
        # formatted_types = [ poke_type["type"]["name"] for poke_type in response["types"] ]
        pokemon_type = "/".join(formatted_types)
        
        # とくせいが複数ある場合の処理
        formatted_abilities = [ translate_info("ability", ability["ability"]["name"]) + "*" if ability["is_hidden"] else translate_info("ability", ability["ability"]["name"]) for ability in response["abilities"] ]
        # formatted_abilities = [ ability["ability"]["name"] + "*" if ability["is_hidden"] else ability["ability"]["name"] for ability in response["abilities"] ]
        abilities = "/".join(formatted_abilities)
        
        
        result = {
            "id": response["id"],
            "名前": translate_info("pokemon-species", response["name"]),
            # "名前": response["name"],
            "タイプ": pokemon_type,
            "とくせい": abilities,
            "HP": response['stats'][0]['base_stat'],
            "こうげき": response['stats'][1]['base_stat'],
            "ぼうぎょ": response['stats'][2]['base_stat'],
            "とくこう": response['stats'][3]['base_stat'],
            "とくぼう": response['stats'][4]['base_stat'],
            "すばやさ": response['stats'][5]['base_stat'],
            "画像URL": response["sprites"]["other"]['official-artwork']['front_default']
        }
        
        print(result)
        
        response_body = {"application/json": {"body": json.dumps(result, ensure_ascii=False)}}
        
        action_response = {
            "actionGroup": event["actionGroup"],
            "apiPath": event["apiPath"],
            "httpMethod": event["httpMethod"],
            "httpStatusCode": 200,
            "responseBody": response_body,
        }
        
    else:
        response_body = {"application/json": {"body": json.dumps({"errorMessage": "エラーが発生しました"}, ensure_ascii=False)}}
        
        action_response = {
            "actionGroup": event["actionGroup"],
            "apiPath": event["apiPath"],
            "httpMethod": event["httpMethod"],
            "httpStatusCode": 400,
            "responseBody": response_body,
        }
    
    return {
        "messageVersion": "1.0", 
        "response": action_response
    }
```


# Terraformの設定など
　Terraformの設定は以下の通りです。provider.tfに記述しています。
- Terraformのバージョン: **1.7.4**
- リージョン: **us-east-1**
- AWS Providerのバージョン: Agent、Knowledge baseがサポートされた**5.47.0**
- AWS Cloud Control Providerのバージョン: **0.76.0**

**AWS Cloud Control Provider**は、現状のAWS ProviderではKnowledge bases for Amazon Bedrock(以降、Knowledge base)のデータソースがサポートされていないため利用します。また、Agentのコード化のためリージョンは**バージニア北部**を選択しています。

```Terraform:provider.tf
provider "aws" {
  region = "us-east-1"
}

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

terraform {
  required_version = "~> 1.7.4"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.47.0"
    }
    awscc = {
      source  = "hashicorp/awscc"
      version = "~> 0.76.0"
    }
  }
}
```

　tfstateファイルはS3バケットに配置するようにしました。S3バケットは事前にAWSコンソール上で作成しています。

```Terraform:backend.tf
terraform {
  backend "s3" {
    bucket = "tfstateファイルを配置するバケット"
    key    = "terraform/bedrock_iac.tfstate"
    region = "ap-northeast-1"
  }
}
```

# Knowledge base のコード化
　準備が出来ましたので、実際にKnowledge baseのコードを見てみたいと思います。以下の記事を参考にさせていただきました。bedrock_kb.tfに定義を記述しています。

https://dev.classmethod.jp/articles/create-knowledge-bases-for-amazon-bedrock-via-terraform
 
```Terraform: bedrock_kb.tf
resource "aws_bedrockagent_knowledge_base" "this" {
  name     = "pokemon-knowledge-base"
  role_arn = aws_iam_role.knowledge_base.arn

  knowledge_base_configuration {
    type = "VECTOR"
    vector_knowledge_base_configuration {
      embedding_model_arn = data.aws_bedrock_foundation_model.embedding.model_arn
    }
  }

  storage_configuration {
    type = "PINECONE"
    pinecone_configuration {
      connection_string      = jsondecode(data.aws_secretsmanager_secret_version.pinecone_url.secret_string)["url"]
      credentials_secret_arn = data.aws_secretsmanager_secret_version.pinecone_apikey.arn
      field_mapping {
        text_field     = "pokemon-info"
        metadata_field = "metadata"
      }
    }
  }

  depends_on = [aws_iam_role_policy_attachment.knowledge_base]
}
```
**knowledge_base_configuration**ブロックで埋め込みに利用するモデルを指定します。モデルのARNが必要になるので、事前にコンソール上でモデルを有効化し、datasourceを利用してARNを取得しています。埋め込み用のモデルとしては、今回はCohere社の**Embed Multilingual**を利用します。

```Terraform: datasource.tf
data "aws_bedrock_foundation_model" "embedding" {
  model_id = "cohere.embed-multilingual-v3"
}
```

**storage_configuration**ブロックでは、ベクトルデータベースとしてPineconeを指定しています。pinecone_configurationでさらに以下のような情報が必要になります。Secrets Managerに格納した情報は、detasourceを利用して取得しています。
- **connection_string** : Pineconeでデータベース作成時に控えておいたHOST情報。今回はSecrets Managerに格納している
- **credentials_secret_arn**: PineconeのAPIキー。こちらはSecrets Managerへの格納を強制されている
- field_mapping : 今回は詳細を省略

　また、同じファイルにknowledge baseのデータソースの定義も記述しています。今回はS3をデータソースとしてドキュメントを格納しているため、typeは「S3」になります。s3_configuration ではバケットの**ARN**を指定します。（バケット名ではないことに注意！）

```Terraform:bedrock_kb.tf
resource "awscc_bedrock_data_source" "this" {
  name              = "pokemon-data-source"
  knowledge_base_id = aws_bedrockagent_knowledge_base.this.id
  data_source_configuration = {
    type = "S3"
    s3_configuration = {
      bucket_arn = data.aws_s3_bucket.embedding.arn
    }
  }
}
```
　また、knowledge baseのIAMロールに関してですが、こちらはiam.tfで以下のように定義しています。AWSのアカウントID情報は、datasourceのaws_caller_identityを利用して取得しています。Bedrockのモデル実行の権限、S3操作の権限、Pineconeの情報をSecrets Managerに格納するので、その操作権限を付与しています。
```Terraform:iam.tf
resource "aws_iam_role" "knowledge_base" {
  name = "knowledge-base-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "bedrock.amazonaws.com"
        }
        Condition = {
          StringEquals = {
            "aws:SourceAccount" = data.aws_caller_identity.this.account_id
          }
          ArnLike = {
            "aws:SourceArn" = "arn:aws:bedrock:us-east-1:${data.aws_caller_identity.this.account_id}:knowledge-base/*"
          }
        }
      }
    ]
  })
}

resource "aws_iam_policy" "knowledge_base" {
  name = "knowledge-base-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = "bedrock:InvokeModel"
        Effect   = "Allow"
        Resource = data.aws_bedrock_foundation_model.embedding.model_arn
      },
      {
        Effect   = "Allow",
        Action   = "s3:ListBucket",
        Resource = data.aws_s3_bucket.embedding.arn,
        Condition = {
          StringEquals = {
            "aws:ResourceAccount" = data.aws_caller_identity.this.account_id
          }
        }
      },
      {
        Effect   = "Allow",
        Action   = "s3:GetObject",
        Resource = "${data.aws_s3_bucket.embedding.arn}/*",
        Condition = {
          StringEquals = {
            "aws:ResourceAccount" = data.aws_caller_identity.this.account_id
          }
        }
      },
      {
        Effect   = "Allow",
        Action   = "secretsmanager:GetSecretValue",
        Resource = "arn:aws:secretsmanager:us-east-1:${data.aws_caller_identity.this.account_id}:secret:*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "knowledge_base" {
  role       = aws_iam_role.knowledge_base.name
  policy_arn = aws_iam_policy.knowledge_base.arn
}
```

　Knowledge baseのコード化は以上になります。コード量がそこまで多くなく、コンソール上で設定する内容を記述するだけですので、比較的簡単にコード化出来ることが分かります。


# Agent のコード化
　続きまして、Agentのコード化になります。こちらに関しては、以下の記事を参考にさせていただきました。bedrock_agent.tfに定義を記述しています。

https://dev.to/aws-builders/how-to-manage-an-amazon-bedrock-agent-using-terraform-1lag

```Terraform: bedrock_agent.tf
resource "aws_bedrockagent_agent" "this" {
  agent_name              = "rotom_pokedex_agent"
  agent_resource_role_arn = aws_iam_role.agent.arn
  description             = "ロトム図鑑を模したAIエージェント"
  foundation_model        = data.aws_bedrock_foundation_model.agent.model_id
  instruction             = <<EOT
あなたはポケモン（ポケットモンスター/pokemon）に詳しい「ロトム図鑑」という図鑑エージェントです。語尾は「〜ロト」、「〜ロ」になります。会話サンプルは以下の通りです
「こんにちはロト〜！」「何か用ロト？」「お役に立つロト〜！」「これからよロトしく！」「マラサダも　スカル団も　両方　気になっちゃうロ……！」
あなたは次のような機能を持っています。
1. 「図鑑番号1000のポケモンは？」のように、図鑑番号を数字で指定した質問をされた場合、該当のポケモンの情報を検索して回答できます。ポケモンの画像のURLも回答に含めて下さい。
2.  図鑑番号の指定がない「ポケモンに関する」質問は Knowledge base を検索して回答します。検索の結果、該当する情報がない場合は以下のように回答します。
<example>ごめんなさいロ... お探しの情報が見つからなかったロト</example>
3. 「こんにちは」など、質問形式の発話ではない場合、そのまま雑談をおこないます。
EOT
}
```
まずは、Agent本体の定義になります。ここでは主に、Agentが実行するモデルとinstructionを定義しています。モデルに関しては、Knowledge baseの時と同様に、事前にモデルを有効化し、datasourceでARNを取得します。モデルはAnthropic社の**Claude 3 Sonnet**を利用します。

```Terraform: datasource.tf
data "aws_bedrock_foundation_model" "agent" {
  model_id = "anthropic.claude-3-sonnet-20240229-v1:0"
}
```

**instruction**でAgentへの指示を記載します。今回は[ロトム図鑑](https://www.pokemon.co.jp/ex/usum/gamesystem/171012_02.html)を意識した内容になっています。今回はヒアドキュメントで直書きしていますが、別ファイルに外出しした方が良い気がします。また、文字数制限があることにも要注意です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/a22063f6-a0d0-ba49-0bfe-71ed08aac8d6.png" width=30%>

AgentのIAMロールの定義は以下になります。Bedrockのモデル、Knowledge baseの実行権限と、S3の操作権限を付与しています。

```Terraform: iam.tf
resource "aws_iam_role" "agent" {
  name = "agent-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "bedrock.amazonaws.com"
        }
        Condition = {
          StringEquals = {
            "aws:SourceAccount" = data.aws_caller_identity.this.account_id
          }
          ArnLike = {
            "aws:SourceArn" = "arn:aws:bedrock:us-east-1:${data.aws_caller_identity.this.account_id}:agent/*"
          }
        }
      }
    ]
  })
}

resource "aws_iam_policy" "agent" {
  name = "agent-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = "bedrock:InvokeModel"
        Effect   = "Allow"
        Resource = data.aws_bedrock_foundation_model.agent.model_arn
      },
      {
        Effect   = "Allow",
        Action   = "bedrock:Retrieve",
        Resource = "arn:aws:bedrock:us-east-1:${data.aws_caller_identity.this.account_id}:knowledge-base/*"
      },
      {
        Effect   = "Allow",
        Action   = "s3:GetObject",
        Resource = "${data.aws_s3_bucket.openapi.arn}/*",
        Condition = {
          StringEquals = {
            "aws:ResourceAccount" = data.aws_caller_identity.this.account_id
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "agent" {
  role       = aws_iam_role.agent.name
  policy_arn = aws_iam_policy.agent.arn
}
```

　次に、**Action Group**の定義です。ここでは、文字通りAgentが実行できる**Action**を設定します。同じくbedrock_agent.tfに記述しています。
```Terraform: bedrock_agent.tf
resource "aws_bedrockagent_agent_action_group" "this" {
  action_group_name          = "invoke-poke-api"
  agent_id                   = aws_bedrockagent_agent.this.id
  agent_version              = "DRAFT"
  skip_resource_in_use_check = true
  action_group_executor {
    lambda = aws_lambda_function.poke_api.arn
  }
  api_schema {
    s3 {
      s3_bucket_name = data.aws_s3_bucket.openapi.id
      s3_object_key  = "invoke_pokeapi_v2.yml"
    }
  }
}
```
**action_group_executor**では、Agentが実行するActionの中身となるLambda（のARN）を指定します。Lambda関連のTerraformの定義は以下になります。

```Terraform: lambda.tf
resource "aws_lambda_function" "poke_api" {
  function_name    = "poke-api"
  role             = aws_iam_role.action_lambda.arn
  filename         = data.archive_file.poke_api.output_path
  source_code_hash = data.archive_file.poke_api.output_base64sha512
  handler          = "index.lambda_handler"
  runtime          = "python3.12"
  timeout          = 300
}

resource "aws_lambda_permission" "poke_api" {
  action         = "lambda:invokeFunction"
  function_name  = aws_lambda_function.poke_api.function_name
  principal      = "bedrock.amazonaws.com"
  source_account = data.aws_caller_identity.this.account_id
  source_arn     = "arn:aws:bedrock:us-east-1:${data.aws_caller_identity.this.account_id}:agent/*"
}
```
事前準備で説明したソースコードをsrc/poke_apiディレクトリ配下に配置しているので、datasourceのarchive_fileを利用してsrc/poke_apiをzip化し、デプロイするように記述しています。また、BedrockがLambdaを実行出来るようにパーミッションを付けています。

　Action Groupsに戻りまして、次に**api_schema**です。AWS Provider v5.48.0現在では、残念ながら**Define with function details がサポートされていないので、APIスキーマでAction Groupsを定義する必要があります。**

```Terraform:再掲
api_schema {
    s3 {
      s3_bucket_name = data.aws_s3_bucket.openapi.id
      s3_object_key  = "invoke_pokeapi_v2.yml"
    }
}
```
以下のようなOpenAIスキーマのyamlファイルを作成しました。このファイルを事前に作成したS3に格納し、api_schemaの定義で、格納先のS3バケットとファイル名を指定しています。

```yaml
openapi: 3.0.0
info:
  title: "invoke pokeapi API"
  version: 1.0.0
  description: "APIs for testing Agents' behavior"
paths:
  "/search":
    get:
      summary: "invoke pokeapi"
      description: "質問の中から図鑑番号に対応する数字文字列を抽出し、それを元にAPIを実行して情報を取得し、返します。id、名前、タイプ、とくせい、HP、こうげき、ぼうぎょ、とくこう、とくぼう、すばやさ、画像URLをJSONで返します。質問の中に図鑑番号が含まれていない場合は、APIは実行せず、Knowledge base  を検索して回答します。回答をする際、次の会話サンプルを参考にして話し方を模倣して下さい。「こんにちはロト〜！」「何か用ロト？」「お役に立つロト〜！」「これからよロトしく！」「ケテー！　呼ばれた！　学校の2階に　行くロ！」「マラサダも　スカル団も　両方　気になっちゃうロ……！」検索の結果、該当する情報がない場合は次のように回答します。<example>ごめんなさいロ... お探しの情報が見つからなかったロト...</example>"
      operationId: search
      parameters:
        - name: pokemon_id
          in: path
          description: "ポケモンの図鑑番号"
          required: true
          schema:
            type: string
      responses:
        200:
          description: "検索成功"
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: "ポケモンの図鑑番号"
                  name:
                    type: string
                    description: "ポケモンの名前"
                  type:
                    type: string
                    description: "ポケモンのタイプ"
                  ability:
                    type: string
                    description: "ポケモンのとくせい"
                  hp:
                    type: string
                    description: "ポケモンのHPの種族値"
                  attack:
                    type: string
                    description: "ポケモンのこうげきの種族値"
                  defense:
                    type: string
                    description: "ポケモンのぼうぎょの種族値"
                  special_attack:
                    type: string
                    description: "ポケモンのとくこうの種族値"
                  special_defense:
                    type: string
                    description: "ポケモンのとくぼうの種族値"
                  speed:
                    type: string
                    description: "ポケモンのすばやさの種族値"
                  image_url:
                    type: string
                    description: "ポケモンの画像URL"
```

　ここまでで、Action Groupsのコードの記述は終わりましたので、続いてはKnowledga baseの紐付けです。**aws_bedrockagent_agent_knowledge_base_association**で、今まで定義してきたAgentとKnowledge baseのIDを指定します。また、descriptionにAgentへの指示を簡単に記述します。（こちらも文字数制限があることに注意！ ）

```Terraform: bedrock_agent.tf
resource "aws_bedrockagent_agent_knowledge_base_association" "this" {
  agent_id             = aws_bedrockagent_agent.this.id
  description          = <<EOT
  ポケモン（ポケットモンスター/pokemon）に関して、図鑑番号の指定がない質問に対する回答はKnowledge base を検索して回答します。
  EOT
  knowledge_base_id    = aws_bedrockagent_knowledge_base.this.id
  knowledge_base_state = "ENABLED"

  depends_on = [aws_bedrockagent_knowledge_base.this]
}
```
　最後に、**エイリアス**の定義の記述になります。AgentをSDK等を用いて呼び出す場合
 、エイリアスを作成してそのIDを指定する必要がありますので、こちらもコードで管理しておきます。

```Terraform: bedrock_agent.tf
resource "aws_bedrockagent_agent_alias" "this" {
  agent_id         = aws_bedrockagent_agent.this.id
  agent_alias_name = "pokedex_agent_alias"
}
```

　今回の記事では触れませんが、Agentの**詳細プロンプト**の設定に関しては、リソースaws_bedrockagent_agent の **prompt_override_configuration** で記述することが出来るようです。詳しくはTerraformの公式をご覧ください。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/bedrockagent_agent

　以上で、Agentのコードについては以上になります。こちらもKnowledge baseと同じく、コンソール上から作成する場合に設定する内容がほぼそのままコードに反映されているので、直感的に理解しやすいかと思います。


# 動作確認
　ではコード化が完了したので、実際にデプロイしてみたいと思います。terraformディレクトリ下で**terraform apply**を実行した結果（一部）が以下になります。
```
...
aws_bedrockagent_agent.this: Creation complete after 4s [id=STCFZIJZYO]
aws_bedrockagent_agent_knowledge_base_association.this: Creating...
aws_bedrockagent_agent_alias.this: Creating...
aws_bedrockagent_agent_knowledge_base_association.this: Creation complete after 0s [id=STCFZIJZYO,DRAFT,5ZQJIOHTZH]
aws_bedrockagent_agent_alias.this: Creation complete after 5s [id=YL6TM1LHQU,STCFZIJZYO]
aws_lambda_function.poke_api: Still creating... [10s elapsed]
aws_lambda_function.poke_api: Creation complete after 16s [id=poke-api]
aws_lambda_permission.poke_api: Creating...
aws_bedrockagent_agent_action_group.this: Creating...
aws_lambda_permission.poke_api: Creation complete after 0s [id=terraform-20240505145602953600000005]
aws_bedrockagent_agent_action_group.this: Creation complete after 1s [id=QHCY1VSAZA,STCFZIJZYO,DRAFT]

Apply complete! Resources: 18 added, 0 changed, 0 destroyed.
```

　**無事Applyに成功したようです！** では、BedrockのコンソールでAgentを確認してみましょう...

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/fafb8a1e-8b35-b84d-3ce7-1cb1aa3d85f0.png">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/e15a56d0-e464-3371-1e42-4d9f0bd71111.png">

問題なく作成されていそうです！ エイリアスも作成されてる！　また、記事スペースの都合上省略しますが、「Edit in Agent Builder」から Action Groupの設定や Knowledge baseの紐付けがデプロイされていることも確認出来ました。
　Knowledge baseの方も見てみると、こちらも大丈夫そうでした！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/78feb907-3b82-3483-a8ae-159748ccca9f.png">

ただし、Knowledge baseに関しては一つ注意点がありまして、**データソースの同期**だけはデプロイ時に実行することが出来ないので、これだけは手動対応が必要になります。「データソース」欄の「**同期**」を押下して実行して下さい。AWS CLIから同期を実行出来るのであれば、**null_resource**を利用してデプロイ時に実行させられるのでは、と考えたのですが、そもそもAWS CLIに同期のコマンドが見当たりませんでした...。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/2ca4c52f-16e7-164d-241f-ed6080589fca.png" width=70%>


　リソースがデプロイされていることが確認出来ましたので、プレイグラウンドからAgentを実行してみます。まずはロトム図鑑に挨拶をしてみます。
 

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/08d1a8c7-a7a9-2e45-2d3b-8b03ab0938e5.png" width=60%>

返事が返ってきました！ 少しメタい内容が含まれていますが、ロトム図鑑のキャラクターもしっかり反映されていそうです。
　次に、Action Groupsの動作確認をしたいと思います。図鑑番号1002番のポケモンについて聞いてみたいと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/726276cc-d7b9-1eba-e332-647ccb077f4b.png" width=100%>


図鑑番号1002番の「パオジアン」というポケモンのデータが返ってきました。画像URLに関しても、リンクにアクセスするとパオジアンの画像が表示されます。トレース情報を見ても、Agentが質問の内容を理解してActionを実行していることが分かります。
https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1002.png

　最後にKnowledge baseの動作も確認します。対戦環境でどのようなパオジアンが使われているか聞いてみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/523ec823-c289-6abb-8341-e613049bc960.png">

怪しいところもありますが、ほぼ正しいことを答えてくれているかと思われます！ 
参考までにデータソースのドキュメントの該当箇所を下に掲載していますが、パオジアンの物理アタッカー型の性格は「いじっぱり」も採用候補になっています。

▼ 参考：データソースのドキュメントの該当箇所
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3026734/ae7c588c-0252-d8bb-fcac-e7ff1af2423d.png" width=80%>

トレースを見ても、質問に対して**Action Groupsでは対応出来ないので Knowledge base を検索する**、という判断をAgentが取っていることが分かります。

以上が動作確認の結果になりますが、特に問題なくデプロイされていそうでした！


# 最後に
　今回は Agents for Amazon Bedrock の IaC に挑戦してみました。いち早く試された方が記事を残していただいていて参考に出来たのに加え、基本的にはAWSコンソール上で作成する手順がそのままTerraformに落とし込まれている感じですので、コード化はそれほど難しくありませんでした。  
　現行のAWS Providerのバージョンでは、まだBedrockの最新の機能はサポートされていませんが、今後のバージョンアップで徐々に追いついてくるはずですので、しっかりウォッチしていきたいと思います！
　今回は以上になります。最後までご精読いただきまして、ありがとうございました！