Agents for Amazon BedrockをCloudFormationでデプロイするときのポイント

## はじめに
Agents for Amazon Bedrock と Knowledge Base for Amazon Bedrock が CloudFormation によるデプロイをサポートしました。

https://aws.amazon.com/jp/about-aws/whats-new/2024/04/knowledge-bases-amazon-bedrock-formation-quotas/

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Bedrock.html

以前 Agents for Amazon Bedrock を活用した Slack アプリを作成する記事を投稿したのですが、Agent はコンソール操作で作成していました。これが Agent 用の Lambda 関数などと一緒に一つのテンプレートでデプロイできるようになります。便利ですね！

https://qiita.com/hayao_k/items/850cb5ef717de7df99f9

この記事では CloudFormation 化する際に引っ掛かったポイントや、知っておくと良い点などを紹介します。

以下で公開しているテンプレートは Agent も CloudFormation で作成できるよう更新済です。[記事の最後](https://qiita.com/hayao_k/items/0764c64cdeaedc67d11e#%E5%8F%82%E8%80%83-%E4%BB%8A%E5%9B%9E%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E5%85%A8%E4%BD%93) にも載せています。

https://github.com/hayao-k/aws-update-navigator



## Ref や Fn::GetAtt で取得できる戻り値あれこれ
組み込み関数の `Ref` や `Fn::GetAtt` を活用すると、ARN や ID などの値が取得でき便利です。戻り値の一覧は [テンプレートリファレンス](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Bedrock.html) を参照いただければと思いますが、特にエージェントおよびエイリアスの ID/ARN は使用頻度が高いのではないかと思います。
| リソース | 宣言例 | 取得できる値 |
| --- | --- | --- |
| AWS::Bedrock::Agent | !Ref ResourceName | エージェントの ID |
| AWS::Bedrock::Agent | !GetAtt ResourceName.AgentArn | エージェントの ARN |
| AWS::Bedrock::AgentAlias | !GetAtt ResourceName.AgentAliasId | エージェントのエイリアス ID|
| AWS::Bedrock::AgentAlias | !GetAtt ResourceName.AgentAliasArn| エージェントのエイリアス ARN |

例えば Agent のアクショングループに設定する Lambda 関数には Agent からの呼び出しを許可するリソースベースポリシーが必要です。以下のように記述できます。

```yaml
Resources:
  BedrockAgentFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !Ref BedrockAgentFunction
      Principal: "bedrock.amazonaws.com"
      SourceArn: !GetAtt BedrockAgent.AgentArn
```

他にも Agent を呼び出す Lambda 関数を定義する場合、呼び出す対象のエージェント ID と エイリアス ID が必要です。以下のように環境変数として定義しておくことで、Lambda 関数内から参照可能になります。また Lambda 関数の実行ロールのポリシーで Resource を制限したい場合、エイリアス ARN を指定します。

```yaml
Resources:
  SlackAppFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: !Sub "${AWS::StackName}-SlackAppFunction"
      CodeUri: "slack_app/"
      Environment:
        Variables:
          BEDROCK_AGENT_ID: !Ref BedrockAgent
          BEDROCK_AGENT_ALIAS_ID: !GetAtt BedrockAgentAlias.AgentAliasId
      Policies:
        - Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - "bedrock:InvokeAgent"
              Resource:
                - !GetAtt BedrockAgentAlias.AgentAliasArn
```

## Agent 用のサービスロール
Agent へ設定するサービスロールに必要な権限や信頼ポリシーは以下のドキュメントを参照してください。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/agents-permissions.html

CloudFormation で作成する場合、例として以下のような定義となります。

```yaml
Parameters:
  pModelId:
    Type: String
    Description: The Model ID of the Bedrock Agent.
    Default: "anthropic.claude-v2:1"
    AllowedValues: 
        - "anthropic.claude-v2:1"
        - "anthropic.claude-v2"
        - "anthropic.claude-instant-v1"

Resources:
  BedrockAgentRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: "Policy for Bedrock Agent"
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action:
              - "bedrock:InvokeModel"
            Resource:
              - !Sub "arn:aws:bedrock:${AWS::Region}::foundation-model/${pModelId}"

  BedrockAgentRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "AmazonBedrockExecutionRoleForAgents_${AWS::StackName}"
      ManagedPolicyArns: 
        - !Ref BedrockAgentRolePolicy
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: "bedrock.amazonaws.com"
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref "AWS::AccountId"
              ArnLike:
                aws:SourceArn: !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/*"
```

ロール名の Prefix が `AmazonBedrockExecutionRoleForAgents_` である必要があります。これに従っていない場合、Agent 作成時に次のようなエラーが発生します。

```
Properties validation failed for resource BedrockAgent with message:                  
#/AgentResourceRoleArn: failed validation constraint for keyword [pattern]
```

Agent が使用する機能により、ポリシーで許可すべきアクションは異なります。最低限必要なのが `bedrock:InvokeModel` で、使用していない機能に対するアクションは省略できます。

例えば OpenAPI の Schema ファイルを S3 に配置している場合は対象のオブジェクトに対する権限が必要ですし、ナレッジーベースを使用する Agent の場合はナレッジベースにアクセスする権限が必要です。

## OpenAPI Schema の指定方法
### S3 にアップロードしたオブジェクトを指定する
バケット名とオブジェクトキーを指定します。
```yaml
Resources:
  BedrockAgent:
    Type: AWS::Bedrock::Agent
    Properties:
      AgentName: !Ref "AWS::StackName"
      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn
      FoundationModel: !Ref pModelId
      Instruction: |
        As an AWS Principal Engineer, you have been assigned the following tasks:
        #以降省略
      ActionGroups:
        - ActionGroupName: "explainLatestAWSUpdate"
          ActionGroupExecutor:
            Lambda: !GetAtt BedrockAgentFunction.Arn
          ApiSchema:
            S3:
              S3BucketName: "apischema-s3"
              S3ObjectKey: "ApiSchema.json"
```

### テンプレートに直接埋め込む
JSON or YAML 型式で直接埋め込むこともできます。

```yaml
  BedrockAgent:
    Type: AWS::Bedrock::Agent
    Properties:
      AgentName: !Ref "AWS::StackName"
      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn
      FoundationModel: !Ref pModelId
      Instruction: |
        As an AWS Principal Engineer, you have been assigned the following tasks:
        #以降省略
      ActionGroups:
        - ActionGroupName: "explainLatestAWSUpdate"
          ActionGroupExecutor:
            Lambda: !GetAtt BedrockAgentFunction.Arn
          ApiSchema:
            Payload: |
              openapi: 3.0.0
              info:
                title: Summarize an article API
                version: 1.0.0
                description: APIs for summarize an article from URL.
              paths:
                /summarize_article:
                  post:
                    summary: APIs for summarize an article from URL.
                    #以降省略
```

## Function Schema の指定方法
2024/4/23 のアップデートで新たに使用可能になったエージェントの定義方法です。Function Schema については以下の記事でも紹介しています。

https://qiita.com/hayao_k/items/57cf2e7950c04e88895a

CloudFormation で定義する場合は以下のようになります。

```yaml
Resources:
  BedrockAgent:
    Type: AWS::Bedrock::Agent
    Properties:
      AgentName: !Ref "AWS::StackName"
      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn
      FoundationModel: !Ref pModelId
      Instruction: |
        As an AWS Principal Engineer, you have been assigned the following tasks:
        #以降省略
      ActionGroups:
        - ActionGroupName: "explainLatestAWSUpdate"
          ActionGroupExecutor:
            Lambda: !GetAtt BedrockAgentFunction.Arn
          FunctionSchema:
            Functions:
              - Name: "summarize_article"
                Description: "summarize an article from url."
                Parameters: 
                  url:
                    Description: "url of article"
                    Required: false
                    Type: "string"
```


## ユーザー入力の有効化/無効化
エージェントの設定にあるこれです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/db87a012-7341-256c-64cd-1301fdb7777a.png)

ドキュメントによれば、この機能を有効にするには `AWS::Bedrock::Agent AgentActionGroup` の `ParentActionGroupSignature` プロパティに `AMAZON.UserInput` を指定したアクショングループを作成する必要があります。その際、`Description`、`ApiSchema`、`ActionGroupExecutor` は空白にする必要があります。例として以下のような定義となります。

```yaml
Resources:
  BedrockAgent:
    Type: AWS::Bedrock::Agent
    Properties:
      AgentName: !Ref "AWS::StackName"
      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn
      FoundationModel: !Ref pModelId
      Instruction: |
        As an AWS Principal Engineer, you have been assigned the following tasks:
        #以降省略
      ActionGroups:
        - ActionGroupName: "userInputAction"
          ActionGroupState: "ENABLED"
          ParentActionGroupSignature: "AMAZON.UserInput"
        - ActionGroupName: "explainLatestAWSUpdate"
          ActionGroupExecutor:
            Lambda: !GetAtt BedrockAgentFunction.Arn
          ApiSchema:
            Payload: |
              openapi: 3.0.0
              #以降省略
```

:::note warn
上記でデプロイは正常に完了するのですが、コンソール上のユーザー入力は「いいえ (DISABLED)」のままになります。もしかするとテンプレートの定義の仕方が間違っているかもしれません。
:::

## AWS::Bedrock::AgentAlias での Agent バージョンの指定

`AWS::Bedrock::AgentAlias` の `RoutingConfiguration` というプロパティでエイリアスに紐づけるエージェントのバージョンを指定できます。

```yaml
Resources:
  BedrockAgentAlias:
    Type: AWS::Bedrock::AgentAlias
    Properties:
      AgentAliasName: "Default"
      AgentId: !Ref BedrockAgent
      RoutingConfiguration:
        - AgentVersion: "1"
```

こんな時、CloudFormation を嗜む皆さんであれば以下のように書きたくなる方もいるのではないでしょうか。(`- AgentVersion: !GetAtt BedrockAgent.AgentVersion`)

```yaml
Resources:
  BedrockAgent:
    Type: AWS::Bedrock::Agent
    Properties:
      AgentName: !Ref "AWS::StackName"
      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn
      AutoPrepare: true
      FoundationModel: !Ref pModelId
      # 以降省略

  BedrockAgentAlias:
    DependsOn: BedrockAgent
    Type: AWS::Bedrock::AgentAlias
    Properties:
      AgentAliasName: "Default"
      AgentId: !Ref BedrockAgent
      RoutingConfiguration:
        - AgentVersion: !GetAtt BedrockAgent.AgentVersion     
```

しかしこのような定義でスタック作成するとエラーになります。Agent 作成後に存在するバージョンは 「作業中のドラフト」を示す `DRAFT` のみであり、Fn::GetAtt で取得される値も `DRAFT` です。このバージョンは `TestAlias` というエイリアスにのみ紐づけが可能であるため、次のようなエラーが発生します。

```
The attribute routingConfiguration in AgentAlias is invalid. 
DRAFT must not be associated with this alias. 
````

エイリアスを新規作成するとエージェントのバージョンも自動で作成されるため、Agent 新規作成時は`RoutingConfigration` プロパティの設定は不要です。

## CloudFormation で Agent のバージョンをどう管理するか
Agent に対する更新を行った場合、その変更は作業中のドラフトに反映されます。運用環境に更新を反映するには新しいバージョンを作成し、そのバージョンを指すエイリアスを呼び出すようにアプリケーションを設定します。

最初は CloudFormation で `AWS::Bedrock::Agent` リソースを更新したら自動でバージョンもあがるのかな？と思いましたがどうやらそういうことではないようです。`AutoPrepare` というプロパティもありますが、これはあくまでエージェントの DRAFT バージョンを自動的に更新するかどうかを指定するものでした。

> Specifies whether to automatically update the DRAFT version of the agent after making changes to the agent. The DRAFT version can be continually iterated upon during internal development. By default, this value is false.

検証したところ、`AWS::Bedrock::AgentAlias` リソースを更新することでバージョンの発行が行われることを確認しました。

```yaml
Resources:
  DependsOn: BedrockAgent
  BedrockAgentAlias:
    Type: AWS::Bedrock::AgentAlias
    Properties:
      AgentAliasName: "Default"
      AgentId: !Ref BedrockAgent
      Description: "Enter some information about the updates and issue a new version."
      Tags:
        Key: "Value"
```

つまり Agent の更新が発生した際には `AgentAliasName`, `Description`, `Tags` といった項目のいずれかを同時に更新することで新しいバージョンが発行され、エイリアスに紐づけられます。AgentAliasName を固定で運用したい場合は `Description` まはた `Tags` に都度 Agent 更新内容にに関する情報を入力するなどして、AgentAlias リソースの更新を行うとよいのではないかと思います。


## 参考: 今回作成したテンプレート全体
<details><summary>AWS SAM テンプレート (クリックで展開)</summary>

```yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: >
  aws-update-navigator

  Sample SAM Template for aws-update-navigator
  
Parameters:
  pModelId:
    Type: String
    Description: The Model ID of the Bedrock Agent.
    Default: "anthropic.claude-3-sonnet-20240229-v1:0"
    AllowedValues: 
        - "anthropic.claude-3-opus-20240229-v1:0"
        - "anthropic.claude-3-sonnet-20240229-v1:0"
        - "anthropic.claude-3-haiku-20240307-v1:0"
        - "anthropic.claude-v2:1"
        - "anthropic.claude-v2"
        - "anthropic.claude-instant-v1"
        - "amazon.titan-text-premier-v1:0"
    
Globals:
  Function:
    Handler: app.lambda_handler
    Runtime: python3.12
    Architectures:
     - x86_64
    LoggingConfig:
      LogFormat: JSON
      ApplicationLogLevel: INFO

Resources:
  SlackAppFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: !Sub "${AWS::StackName}-SlackAppFunction"
      CodeUri: "slack_app/"
      MemorySize: 512
      Timeout: 180
      FunctionUrlConfig:
        AuthType: NONE
      Environment:
        Variables:
          BEDROCK_AGENT_ID: !Ref BedrockAgent
          BEDROCK_AGENT_ALIAS_ID: !GetAtt BedrockAgentAlias.AgentAliasId
          SLACK_BOT_TOKEN: "dummy"
          SLACK_SIGNING_SECRET: "dummy"
      Layers:
        - !Ref SlackAppLayer
      Policies:
        - Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - "bedrock:InvokeAgent"
              Resource:
                - !GetAtt BedrockAgentAlias.AgentAliasArn
            - Effect: Allow
              Action:
               - "lambda:InvokeFunction"
              Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-SlackAppFunction"
  
  SlackAppFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${SlackAppFunction}"
      RetentionInDays: 14
  
  SlackAppLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: "boto3 & slack_bolt"
      ContentUri: "slack_app_layer/"
      CompatibleRuntimes:
        - python3.12
    Metadata:
      BuildMethod: python3.12
      BuildArchitecture: x86_64
  
  BedrockAgentFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: !Sub "${AWS::StackName}-BedrockAgentFunction"
      CodeUri: "bedrock_agent/"
      MemorySize: 128
      Timeout: 10
      Layers:
        - !Ref BedrockAgentFunctionLayer
  
  BedrockAgentFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !Ref BedrockAgentFunction
      Principal: "bedrock.amazonaws.com"
      SourceArn: !GetAtt BedrockAgent.AgentArn
  
  BedrockAgentFunctionLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      Description: "beautifulsoup4"
      ContentUri: "bedrock_agent_layer/"
      CompatibleRuntimes:
        - python3.12
    Metadata:
      BuildMethod: python3.12
      BuildArchitecture: x86_64

  BedrockAgentFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${BedrockAgentFunction}"
      RetentionInDays: 14

  BedrockAgentRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: "Policy for Bedrock Agent"
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action:
              - "bedrock:InvokeModel"
            Resource:
              - !Sub "arn:aws:bedrock:${AWS::Region}::foundation-model/${pModelId}"

  BedrockAgentRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "AmazonBedrockExecutionRoleForAgents_${AWS::StackName}"
      ManagedPolicyArns: 
        - !Ref BedrockAgentRolePolicy
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: "bedrock.amazonaws.com"
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref "AWS::AccountId"
              ArnLike:
                aws:SourceArn: !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/*"
              
  BedrockAgent:
    Type: AWS::Bedrock::Agent
    Properties:
      AgentName: !Ref "AWS::StackName"
      AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn
      AutoPrepare: true
      FoundationModel: !Ref pModelId
      IdleSessionTTLInSeconds: 120
      ActionGroups:
        - ActionGroupName: "explainLatestAWSUpdate"
          ActionGroupExecutor:
            Lambda: !GetAtt BedrockAgentFunction.Arn
          FunctionSchema:
            Functions:
              - Name: "summarize_article"
                Description: "summarize an article from url."
                Parameters: 
                  url:
                    Description: "url of article"
                    Required: false
                    Type: "string"
      Instruction: |
        As an AWS Principal Engineer, you have been assigned the following tasks:
      
        1. Access the AWS service update URL and provide a summary of the English text in Japanese. There is no need to categorize the URL content.
        2. Share your thoughts on the update in Japanese, focusing on the following points:
          2.1. Discuss the advantages of this technology or service compared to existing technologies or services, and explain how it achieves these benefits.
          2.2. Describe the technical challenges that this technology or service addresses.
        3. Respond in json format.

        Here’s an example:
        summary, advantages, and addresses are all required fields
        <example>
        {
          "summary": "Amazon EC2シリアルコンソールがすべてのAWSローカルゾーンで利用できるようになりました。",
          "advantages": "インスタンスの起動やネットワークの接続の問題をトラブルシューティングするために、シリアルポートへのテキストベースのアクセスを簡単かつ安全に提供します。これにより、SSHやRDPで接続できない場合でも、対話形式でコマンドを実行して構成の問題を解決できます。",
          "addresses": "これまでも管理コンソールやAPIを通じてシリアルコンソール出力にアクセスできましたが、それらは主に診断のためで、対話式のトラブルシューティングには適していませんでした。この新機能により、こうした制限がなくなり、はるかに使いやすくなります。"
        }
        </example>

        If the tool did not return a summary, reply "Could not retrieve.".
        
  BedrockAgentAlias:
    DependsOn: BedrockAgent
    Type: AWS::Bedrock::AgentAlias
    Properties:
      AgentAliasName: "Default"
      AgentId: !Ref BedrockAgent

Outputs:
  SlackAppFunctionUrl:
    Description: "Function URL Endpoint"
    Value: !GetAtt SlackAppFunctionUrl.FunctionUrl
```

</details>

以上です。
参考になれば幸いです。