Amazon Q Developer CLIを用いた検証環境のIaC化

こんにちは、ふくちと申します。

Amazon Q Developer CLI、使ってらっしゃいますでしょうか。

AWS環境と人間の新たなインターフェースとして存在する(と私は認識している)Amazon Q Developer CLIですが、個人的には**コンソール上で作成した検証用リソースをサクッとIaC化できる**のがめちゃくちゃ嬉しいです。

私は検証の段階では、IaCでリソースを作成していません。
基本的にマネコンぽちぽちでリソース作成して動作確認することがほとんどです。

ただ、それを本番環境で用いたり、趣味でOSSを作成して公開したりする場合にはIaC化が必須だと思います。
つまり、検証環境で作成したものをコード化する手間があります。

そこをAmazon Q Developer CLIが良しなにやってくれる、というところでめちゃくちゃありがたいなと思ったので、その簡単なやり方をお伝えするとともに、実際にやってみたログを残してみます。

# 検証環境のアーキテクチャ
[JAWS-UG東京 ランチタイムLT会 #23](https://www.youtube.com/watch?v=9i2rwLF3h5k)で登壇させていただいた時のものをCDK化してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/8c520aef-d4bc-4e31-9998-9d103e13512e.png)

https://speakerdeck.com/har1101/ambient-agent-on-aws

# 前提：Amazon Q Developer CLIを使えるようにしておく
本記事では導入方法について記載しません。以下を参考に、導入してみてください。

公式ドキュメント：

https://docs.aws.amazon.com/ja_jp/amazonq/latest/qdeveloper-ug/command-line-installing.html

Windows：

https://qiita.com/moritalous/items/09b7461044f8ff55bc05

https://blog.serverworks.co.jp/amazon-q-for-command-line-wsl

Macbook：

https://dev.classmethod.jp/articles/amazon-q-developer-japanese-language-support-ide-cli/

※これ以降は、Macbookでの操作を前提としています。

また、検証環境にアクセスするための設定も行っておいてください。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-quickstart.html

# 検証環境のAWSリソースを取得し、CDK化してもらう
お好きなエディターを開いていただき、ターミナルでAmazon Q Developerとの対話を始めます。
```terminal:ターミナル
$ q chat

    ⢠⣶⣶⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⣿⣿⣶⣦⡀⠀
 ⠀⠀⠀⣾⡿⢻⣿⡆⠀⠀⠀⢀⣄⡄⢀⣠⣤⣤⡀⢀⣠⣤⣤⡀⠀⠀⢀⣠⣤⣤⣤⣄⠀⠀⢀⣤⣤⣤⣤⣤⣤⡀⠀⠀⣀⣤⣤⣤⣀⠀⠀⠀⢠⣤⡀⣀⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⢠⣿⣿⠋⠀⠀⠀⠙⣿⣿⡆
 ⠀⠀⣼⣿⠇⠀⣿⣿⡄⠀⠀⢸⣿⣿⠛⠉⠻⣿⣿⠛⠉⠛⣿⣿⠀⠀⠘⠛⠉⠉⠻⣿⣧⠀⠈⠛⠛⠛⣻⣿⡿⠀⢀⣾⣿⠛⠉⠻⣿⣷⡀⠀⢸⣿⡟⠛⠉⢻⣿⣷⠀⠀⠀⠀⠀⠀⣼⣿⡏⠀⠀⠀⠀⠀⢸⣿⣿
 ⠀⢰⣿⣿⣤⣤⣼⣿⣷⠀⠀⢸⣿⣿⠀⠀⠀⣿⣿⠀⠀⠀⣿⣿⠀⠀⢀⣴⣶⣶⣶⣿⣿⠀⠀⠀⣠⣾⡿⠋⠀⠀⢸⣿⣿⠀⠀⠀⣿⣿⡇⠀⢸⣿⡇⠀⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⢹⣿⣇⠀⠀⠀⠀⠀⢸⣿⡿
 ⢀⣿⣿⠋⠉⠉⠉⢻⣿⣇⠀⢸⣿⣿⠀⠀⠀⣿⣿⠀⠀⠀⣿⣿⠀⠀⣿⣿⡀⠀⣠⣿⣿⠀⢀⣴⣿⣋⣀⣀⣀⡀⠘⣿⣿⣄⣀⣠⣿⣿⠃⠀⢸⣿⡇⠀⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣦⣀⣀⣀⣴⣿⡿⠃
 ⠚⠛⠋⠀⠀⠀⠀⠘⠛⠛⠀⠘⠛⠛⠀⠀⠀⠛⠛⠀⠀⠀⠛⠛⠀⠀⠙⠻⠿⠟⠋⠛⠛⠀⠘⠛⠛⠛⠛⠛⠛⠃⠀⠈⠛⠿⠿⠿⠛⠁⠀⠀⠘⠛⠃⠀⠀⠘⠛⠛⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⢿⣿⣿⣋⠀⠀
 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠿⢿⡧

╭─────────────────────────────── Did you know? ────────────────────────────────╮
│                                                                              │
│     You can resume the last conversation from your current directory by      │
│                        launching with q chat --resume                        │
│                                                                              │
╰──────────────────────────────────────────────────────────────────────────────╯

/help all commands  •  ctrl + j new lines  •  ctrl + s fuzzy search
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

>
```

まずは以下のように依頼してみました。
```terminal:ターミナル
> 私のAWS環境にある以下のリソースに関する情報を取得し、それをAWS CDKコードに変換してください。
  IAMロールなど権限周りの情報も取得してください。
  ・Lambda: call-agent-function, get-data-from-dynamodb　
  ・Bedrock Agent: evaluate-students-agent　
  ・EventBridge: スケジュール　
  ・CloudFormation: students-evaluation-systemスタックとそれに紐づくAPI Gateway, Lambda, DynamoDB
```

すると、AWS CLIコマンドを実行してAWS環境上にあるリソースの情報を取得し、最終的には以下のようなCDKコードを作成してくれました。

```terminal:実行結果
AWS環境にある指定されたリソースの情報を取得します。IAMロールなど権限周りの情報も含めて調査します。

🛠️  Using tool: use_aws (trusted)
 ⋮ 
 ● Running aws cli command:

Service name: lambda
Operation name: get-function
Parameters: 
- FunctionName: "call-agent-function"
Profile name: default
Region: us-east-1
Label: Lambda関数「call-agent-function」の詳細取得
 ⋮ 
 ● Completed in 1.282s

🛠️  Using tool: use_aws (trusted)
 ⋮ 
 ● Running aws cli command:

Service name: lambda
Operation name: get-function
Parameters: 
- FunctionName: "get-data-from-dynamodb"
Profile name: default
Region: us-east-1
Label: Lambda関数「get-data-from-dynamodb」の詳細取得
 ⋮ 
 ● Completed in 1.112s

🛠️  Using tool: use_aws (trusted)
 ⋮ 
 ● Running aws cli command:

Service name: iam
Operation name: get-role
Parameters: 
- RoleName: "call-agent-function-role-<id>"
Profile name: default
Region: us-east-1
Label: IAMロール「call-agent-function-role-<id>」の詳細取得
 ⋮ 
 ● Completed in 1.247s

...(CDKコードが生成されました。長いので後述)...

このCDKコードは、収集した情報に基づいて以下のリソースを作成します：

1. DynamoDB テーブル:
   • テーブル名: students-evaluation
   • パーティションキー: students_id (文字列)
   • ソートキー: period (文字列)
   • オンデマンド課金モード

2. Lambda 関数:
   • call-agent-function: Bedrock エージェントを呼び出す関数
   • get-data-from-dynamodb: DynamoDB からデータを取得する関数
   • students-evaluation-api-prod: API Gateway と統合された関数

3. IAM ロール:
   • Bedrock エージェント用ロール
   • Lambda 関数用ロール（各関数ごと）
   • EventBridge Scheduler 用ロール

4. API Gateway:
   • REST API: students-evaluation-api-prod
   • /evaluation リソースに GET と PUT メソッド

5. EventBridge Scheduler:
   • 毎日 11:50 (JST) に call-agent-function を実行

6. SNS トピック:
   • エージェント完了通知用

7. Bedrock エージェント:
   • 名前: evaluate-students-agent
   • モデル: Claude 3.5 Sonnet

注意点：
• Bedrock エージェントの完全な設定は CDK でサポートされていないため、一部の設定はコンソールまたは CLI で行
う必要があります。
• 実際のデプロイ前に、各リソースの設定を確認し、必要に応じて調整してください。
```

SNSは以来の中に含めるのを忘れていたんですが、Qが自分で考えて取得してくれていました。

続いて、作成してもらったコードを元に、CDKプロジェクトを作成してもらいます。
```terminal:ターミナル
> cdkフォルダ配下でCDKプロジェクトを作成してください。

CDKプロジェクトを作成します。cdkフォルダ配下にCDKプロジェクトを設定していきましょう。

🛠️  Using tool: execute_bash
 ⋮ 
 ● I will run the following shell command: 
mkdir -p cdk && cd cdk && npx cdk init app --language typescript
 ⋮ 
 ↳ Purpose: CDKプロジェクトをTypeScriptで初期化


Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y
```

cdkフォルダ作成、作業ディレクトリ移動、cdk initコマンド実行を自動でやってくれました。
コマンド実行時には人間側の承認を通してくれるのも嬉しいですね(いわゆるHuman in the loop)。

(本当はライブラリのインストールもしようとしてくれたのですが、そこは自分でやることにしました)

そして、作成したCDKプロジェクトに生成したソースコードを反映してもらいます。
```terminal:ターミナル
> さっき作成したCDKコードを反映させてください

了解しました。先ほど作成したCDKコードをプロジェクトに反映させます。

🛠️  Using tool: fs_read (trusted)
 ⋮ 
 ● Reading file: cdk/lib/cdk-stack.ts, all lines
 ⋮ 
 ● Completed in 0.0s

🛠️  Using tool: fs_write
 ⋮ 
 ● Path: cdk/lib/cdk-stack.ts

   1,   1: import * as cdk from 'aws-cdk-lib';
   2,   2: import { Construct } from 'constructs';
   ...
   15, 438:   }
   16, 439: }

Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y
Updating: cdk/lib/cdk-stack.ts

 ⋮ 
 ● Completed in 0.2s

🛠️  Using tool: fs_read (trusted)
 ⋮ 
 ● Reading file: cdk/bin/cdk.ts, all lines
 ⋮ 
 ● Completed in 0.0s

🛠️  Using tool: fs_write
 ⋮ 
 ● Path: cdk/bin/cdk.ts

   1,  1: #!/usr/bin/env node
   2,  2: import * as cdk from 'aws-cdk-lib';
   3,  3: import { CdkStack } from '../lib/cdk-stack';
   4,  4: 
   5,  5: const app = new cdk.App();
-  6    : new CdkStack(app, 'CdkStack', {
-  7    :   /* If you don't specify 'env', this stack will be environment-agnostic.
-  8    :    * Account/Region-dependent features and context lookups will not work,
-  9    :    * but a single synthesized template can be deployed anywhere. */
- 10    : 
- 11    :   /* Uncomment the next line to specialize this stack for the AWS Account
- 12    :    * and Region that are implied by the current CLI configuration. */
- 13    :   // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
- 14    : 
- 15    :   /* Uncomment the next line if you know exactly what Account and Region you
- 16    :    * want to deploy the stack to. */
- 17    :   // env: { account: '123456789012', region: 'us-east-1' },
- 18    : 
- 19    :   /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
+      6: new CdkStack(app, 'students-evaluation-system', {
+      7:   /* 環境設定を有効化することで、特定のAWSアカウントとリージョンにデプロイできます */
+      8:   env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
+      9:   
+     10:   /* スタックの説明を追加 */
+     11:   description: '学校生徒の評価を行うためのBedrockエージェント基盤',
  20, 12: });


Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y
Updating: cdk/bin/cdk.ts

 ⋮ 
 ● Completed in 0.2s
```

<details><summary>そして最終的に以下ファイルが完成しました。(長いので折りたたんでいます。)</summary>

```cdk-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as sns from 'aws-cdk-lib/aws-sns';

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // パラメータ定義
    const environment = new cdk.CfnParameter(this, 'Environment', {
      type: 'String',
      default: 'prod',
      description: '環境名（開発/本番）'
    });

    const lambdaRuntime = new cdk.CfnParameter(this, 'LambdaRuntime', {
      type: 'String',
      default: 'python3.12',
      description: 'Lambda関数のランタイム'
    });

    // DynamoDBテーブル
    const studentsEvaluationTable = new dynamodb.Table(this, 'StudentsEvaluationTable', {
      tableName: 'students-evaluation',
      partitionKey: { name: 'students_id', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'period', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    // SNSトピック（エージェント完了通知用）
    const finishAgentMailTopic = new sns.Topic(this, 'FinishAgentMailTopic', {
      topicName: 'finish-agent-mail',
    });

    // API Gateway + Lambda用のロール
    const apiLambdaRole = new iam.Role(this, 'LambdaExecutionRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
      ],
    });

    // DynamoDBアクセス権限を追加
    apiLambdaRole.addToPolicy(new iam.PolicyStatement({
      actions: ['dynamodb:GetItem', 'dynamodb:Query', 'dynamodb:UpdateItem'],
      resources: [studentsEvaluationTable.tableArn],
    }));

    // API Gateway統合Lambda関数
    const apiFunction = new lambda.Function(this, 'EvaluationApiFunction', {
      functionName: `students-evaluation-api-${environment.valueAsString}`,
      runtime: lambda.Runtime.of(lambdaRuntime.valueAsString),
      handler: 'index.lambda_handler',
      code: lambda.Code.fromInline(`
import json
import os
import boto3
from datetime import datetime

def lambda_handler(event, context):
    """
    API Gateway経由でDynamoDBのデータを取得・更新するLambda関数
    """
    try:
        # 環境変数からテーブル名を取得
        table_name = os.environ['TABLE_NAME']
        dynamodb = boto3.resource('dynamodb')
        table = dynamodb.Table(table_name)
        
        # HTTPメソッドに応じた処理
        http_method = event['httpMethod']
        
        if http_method == 'GET':
            # クエリパラメータからstudents_idとperiodを取得
            query_params = event.get('queryStringParameters', {}) or {}
            students_id = query_params.get('students_id')
            period = query_params.get('period')
            
            if not students_id or not period:
                return {
                    'statusCode': 400,
                    'body': json.dumps({'message': 'students_id and period are required'})
                }
            
            # DynamoDBからデータを取得
            response = table.get_item(
                Key={
                    'students_id': students_id,
                    'period': period
                }
            )
            
            item = response.get('Item', {})
            return {
                'statusCode': 200,
                'body': json.dumps(item)
            }
            
        elif http_method == 'PUT':
            # リクエストボディからデータを取得
            body = json.loads(event['body'])
            students_id = body.get('students_id')
            period = body.get('period')
            achievements = body.get('achievements', [])
            
            if not students_id or not period or not achievements:
                return {
                    'statusCode': 400,
                    'body': json.dumps({'message': 'students_id, period, and achievements are required'})
                }
            
            # 現在の日時を取得
            current_time = datetime.now().isoformat()
            
            # DynamoDBにデータを保存
            table.put_item(
                Item={
                    'students_id': students_id,
                    'period': period,
                    'achievements': achievements,
                    'updated_at': current_time
                }
            )
            
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Data saved successfully'})
            }
        
        else:
            return {
                'statusCode': 405,
                'body': json.dumps({'message': 'Method not allowed'})
            }
            
    except Exception as e:
        print(f"Error: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({'message': f'Internal server error: {str(e)}'})
        }
      `),
      timeout: cdk.Duration.seconds(30),
      memorySize: 256,
      role: apiLambdaRole,
      environment: {
        TABLE_NAME: studentsEvaluationTable.tableName,
      },
    });

    // API Gateway
    const api = new apigateway.RestApi(this, 'EvaluationApi', {
      restApiName: `students-evaluation-api-${environment.valueAsString}`,
      description: '学生評価システム用API',
      deployOptions: {
        stageName: environment.valueAsString,
      },
    });

    // API Gatewayリソースとメソッド
    const evaluationResource = api.root.addResource('evaluation');
    evaluationResource.addMethod('GET', new apigateway.LambdaIntegration(apiFunction));
    evaluationResource.addMethod('PUT', new apigateway.LambdaIntegration(apiFunction));

    // Bedrock Agent用のロール
    const bedrockAgentRole = new iam.Role(this, 'BedrockAgentRole', {
      roleName: 'AmazonBedrockExecutionRoleForAgents_B0*********',
      assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com', {
        conditions: {
          StringEquals: {
            'aws:SourceAccount': this.account,
          },
          ArnLike: {
            'aws:SourceArn': `arn:aws:bedrock:${this.region}:${this.account}:agent/*`,
          },
        },
      }),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AWSLambda_FullAccess'),
      ],
    });

    // DynamoDBからデータを取得するLambda関数用のロール
    const getDynamoDbRole = new iam.Role(this, 'GetDynamoDbRole', {
      roleName: 'get-data-from-dynamodb-role-6z******',
      path: '/service-role/',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBReadOnlyAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
      ],
    });

    // DynamoDBからデータを取得するLambda関数
    const getDynamoDbFunction = new lambda.Function(this, 'GetDataFromDynamoDb', {
      functionName: 'get-data-from-dynamodb',
      runtime: lambda.Runtime.PYTHON_3_13,
      handler: 'lambda_function.lambda_handler',
      code: lambda.Code.fromInline(`
import json
import boto3
from boto3.dynamodb.conditions import Key

def lambda_handler(event, context):
    """
    Bedrock Agentのアクショングループから呼び出されるLambda関数
    DynamoDBから生徒の評価データを取得する
    """
    try:
        # イベントからパラメータを取得
        students_id = event.get('students_id')
        period = event.get('period')
        
        if not students_id or not period:
            return {
                'statusCode': 400,
                'message': 'students_id and period are required',
                'data': None
            }
        
        # DynamoDBからデータを取得
        dynamodb = boto3.resource('dynamodb')
        table = dynamodb.Table('students-evaluation')
        
        response = table.get_item(
            Key={
                'students_id': students_id,
                'period': period
            }
        )
        
        item = response.get('Item')
        
        if not item:
            return {
                'statusCode': 404,
                'message': f'No data found for student {students_id} in period {period}',
                'data': None
            }
        
        return {
            'statusCode': 200,
            'message': 'Data retrieved successfully',
            'data': item
        }
        
    except Exception as e:
        print(f"Error: {str(e)}")
        return {
            'statusCode': 500,
            'message': f'Internal server error: {str(e)}',
            'data': None
        }
      `),
      timeout: cdk.Duration.seconds(30),
      memorySize: 128,
      role: getDynamoDbRole,
    });

    // エージェント呼び出し用Lambda関数のロール
    const callAgentRole = new iam.Role(this, 'CallAgentRole', {
      roleName: 'call-agent-function-role-hq******',
      path: '/service-role/',
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSNSFullAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonBedrockFullAccess'),
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
      ],
    });

    // エージェント呼び出し用Lambda関数
    const callAgentFunction = new lambda.Function(this, 'CallAgentFunction', {
      functionName: 'call-agent-function',
      runtime: lambda.Runtime.PYTHON_3_13,
      handler: 'lambda_function.lambda_handler',
      code: lambda.Code.fromInline(`
import json
import os
import boto3
import time

def lambda_handler(event, context):
    """
    EventBridge Schedulerから定期的に呼び出されるLambda関数
    Bedrock Agentを呼び出して生徒の評価を行う
    """
    try:
        # 環境変数から設定を取得
        agent_id = os.environ['AGENT_ID']
        agent_alias_id = os.environ['AGENT_ALIAS_ID']
        sns_topic_arn = os.environ['SNS_TOPIC_ARN']
        
        # イベントからパラメータを取得
        students_id = event.get('students_id')
        period = event.get('period')
        
        if not students_id or not period:
            raise ValueError("students_id and period are required")
        
        # Bedrock Agentを呼び出す
        bedrock_agent_runtime = boto3.client('bedrock-agent-runtime')
        
        # エージェントに送信するプロンプト
        prompt = f"生徒ID {students_id} の {period} 期間の評価を行ってください。"
        
        # エージェントを呼び出す
        response = bedrock_agent_runtime.invoke_agent(
            agentId=agent_id,
            agentAliasId=agent_alias_id,
            sessionId=f"{students_id}-{period}-{int(time.time())}",
            inputText=prompt
        )
        
        # レスポンスを処理
        completion = ""
        for event in response.get('completion'):
            chunk = event.get('chunk')
            if chunk and 'bytes' in chunk:
                completion += chunk['bytes'].decode('utf-8')
        
        # 評価結果をSNSで通知
        sns = boto3.client('sns')
        sns.publish(
            TopicArn=sns_topic_arn,
            Subject=f"生徒 {students_id} の評価が完了しました",
            Message=f"期間: {period}\\n\\n評価結果:\\n{completion}"
        )
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Agent invocation successful',
                'students_id': students_id,
                'period': period
            })
        }
        
    except Exception as e:
        print(f"Error: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({
                'message': f'Error invoking agent: {str(e)}'
            })
        }
      `),
      timeout: cdk.Duration.seconds(300),
      memorySize: 1024,
      role: callAgentRole,
      environment: {
        AGENT_ID: '**********',
        AGENT_ALIAS_ID: '**********',
        SNS_TOPIC_ARN: finishAgentMailTopic.topicArn,
      },
    });

    // EventBridge Scheduler用のロール
    const schedulerRole = new iam.Role(this, 'SchedulerRole', {
      roleName: 'Amazon_EventBridge_Scheduler_LAMBDA_52********',
      path: '/service-role/',
      assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com', {
        conditions: {
          StringEquals: {
            'aws:SourceAccount': this.account,
          },
        },
      }),
    });

    // Lambda呼び出し権限を追加
    schedulerRole.addToPolicy(new iam.PolicyStatement({
      actions: ['lambda:InvokeFunction'],
      resources: [
        callAgentFunction.functionArn,
        `${callAgentFunction.functionArn}:*`,
      ],
    }));

    // EventBridge Scheduler (CfnResourceとして定義)
    const schedule = new cdk.CfnResource(this, 'CallAgentSchedule', {
      type: 'AWS::Scheduler::Schedule',
      properties: {
        Name: 'call-agent-schedule',
        Description: '',
        FlexibleTimeWindow: {
          Mode: 'OFF',
        },
        ScheduleExpression: 'cron(50 11 * * ? *)',
        ScheduleExpressionTimezone: 'Asia/Tokyo',
        Target: {
          Arn: callAgentFunction.functionArn,
          RoleArn: schedulerRole.roleArn,
          Input: JSON.stringify({
            students_id: '30000',
            period: '202504'
          }),
          RetryPolicy: {
            MaximumEventAgeInSeconds: 86400,
            MaximumRetryAttempts: 0
          }
        },
        State: 'ENABLED',
      }
    });

    // Bedrock Agent (CfnResourceとして定義)
    // 注: 実際のBedrockエージェントの作成はコンソールまたはCLIで行う必要があります
    const bedrockAgent = new cdk.CfnResource(this, 'BedrockAgent', {
      type: 'AWS::Bedrock::Agent',
      properties: {
        AgentName: 'evaluate-students-agent',
        AgentResourceRoleArn: bedrockAgentRole.roleArn,
        Description: '生徒の成績を評価するAIエージェント',
        FoundationModel: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
        Instruction: '生徒たちの成績を評価する教師エージェントです。定められた評価基準にしたがって、生徒たちの成績をつけていきます。',
        IdleSessionTTLInSeconds: 600,
      }
    });

    // 出力
    new cdk.CfnOutput(this, 'DynamoDBTableName', {
      value: studentsEvaluationTable.tableName,
      description: 'DynamoDBテーブル名',
      exportName: `${this.stackName}-DynamoDBTableName`,
    });

    new cdk.CfnOutput(this, 'ApiEndpoint', {
      value: api.url,
      description: 'API Gatewayエンドポイント',
      exportName: `${this.stackName}-ApiEndpoint`,
    });
  }
}

```
***

</details>

ざっと流し見したところ、コンソールで作業したときと同じような設定になっていそうです。
仮に違っていたとしても、0からファイルを作成するよりも、Qに作成してもらったものを手直しする方が、圧倒的にかかる時間は少なそうです。

# 修正を依頼する
ただ、Bedrock Agentの実装がおかしそうです。
CfnAgentで作成できるんですが、別な実装になっていました。
```cdk-stack.ts
    // Bedrock Agent (CfnResourceとして定義)
    // 注: 実際のBedrockエージェントの作成はコンソールまたはCLIで行う必要があります
    const bedrockAgent = new cdk.CfnResource(this, 'BedrockAgent', {
      type: 'AWS::Bedrock::Agent',
      properties: {
        AgentName: 'evaluate-students-agent',
        AgentResourceRoleArn: bedrockAgentRole.roleArn,
        Description: '生徒の成績を評価するAIエージェント',
        FoundationModel: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
        Instruction: '生徒たちの成績を評価する教師エージェントです。定められた評価基準にしたがって、生徒たちの成績をつけていきます。',
        IdleSessionTTLInSeconds: 600,
      }
    });
```


以下の形で修正を依頼すると、MCPを上手く利用して解決してくれました。
```terminal:ターミナル
> Bedrock Agentはcdk.CfnAgentで作成可能です。ドキュメントを検索したり、Web検索して最新情報を取得し、実装を
修正してください。
I'll search for the latest information about creating Bedrock Agents with CDK and update the 
implementation accordingly.


🛠️  Using tool: brave_web_search from mcp server brave_search
 ⋮ 
 ● Running brave_web_search with the param:
 ⋮  {
 ⋮    "arguments": {
 ⋮      "query": "aws cdk CfnAgent Bedrock Agent implementation example"
 ⋮    },
 ⋮    "name": "brave_web_search"
 ⋮  }
Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y

 ⋮ 
 ● Completed in 1.164s

🛠️  Using tool: brave_web_search from mcp server brave_search
 ⋮ 
 ● Running brave_web_search with the param:
 ⋮  {
 ⋮    "arguments": {
 ⋮      "query": "aws cdk CfnAgent Bedrock Agent action groups example"
 ⋮    },
 ⋮    "name": "brave_web_search"
 ⋮  }
Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y

 ⋮ 
 ● Completed in 1.15s
```

最終的に生成されたコードがこんな感じです。良い感じですね〜
```cdk-stack.ts
    // Bedrock Agent - 正しいCfnAgentクラスを使用
    const bedrockAgent = new bedrock.CfnAgent(this, 'BedrockAgent', {
      agentName: 'evaluate-students-agent',
      agentResourceRoleArn: bedrockAgentRole.roleArn,
      description: '生徒の成績を評価するAIエージェント',
      foundationModel: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
      instruction: '生徒たちの成績を評価する教師エージェントです。定められた評価基準にしたがって、生徒たちの成績をつけていきます。',
      idleSessionTtlInSeconds: 600,
      // アクショングループを定義
      actionGroups: [
        {
          actionGroupName: 'get-students-data',
          description: '評価対象となる生徒の情報をDynamoDBから取得するためのアクショングループです',
          actionGroupState: 'ENABLED',
          actionGroupExecutor: {
            lambda: getDynamoDbFunction.functionArn
          },
          functionSchema: {
            functions: [
              {
                name: 'get-data-from-dynamodb',
                description: '評価対象となる生徒の情報をDynamoDBから取得する関数です。',
                parameters: {
                  students_id: {
                    type: 'string',
                    description: '出席番号',
                    required: true
                  },
                  period: {
                    type: 'string',
                    description: 'データを取得する対象期間',
                    required: true
                  }
                },
                requireConfirmation: 'DISABLED'
              }
            ]
          }
        }
      ]
    });

    // Bedrock Agent Alias
    const bedrockAgentAlias = new bedrock.CfnAgentAlias(this, 'BedrockAgentAlias', {
      agentId: bedrockAgent.attrAgentId,
      agentAliasName: 'v1'
    });
```

また、1箇所エラーが出ていました。
```cdk-stack.ts
    // API Gateway統合Lambda関数
    const apiFunction = new lambda.Function(this, 'EvaluationApiFunction', {
      functionName: `students-evaluation-api-${environment.valueAsString}`,
      // プロパティ 'of' は型 'typeof Runtime' に存在しません
      runtime: lambda.Runtime.of(lambdaRuntime.valueAsString),
      handler: 'index.lambda_handler',
      code: lambda.Code.fromInline(`
      (省略)
```

上記の修正とともに、LambdaのランタイムがCloudFormationスタックパラメータ化されていたのを修正するように依頼しました。
```terminal:ターミナル
    // API Gateway統合Lambda関数
    const apiFunction = new lambda.Function(this, 'EvaluationApiFunction', {
      functionName: `students-evaluation-api-${environment.valueAsString}`,
      runtime: lambda.Runtime.PYTHON_3_13,
      handler: 'index.lambda_handler',
      code: lambda.Code.fromInline(`
```

# もう一歩頑張ってほしい部分
自動生成されたIAMロールには、よく末尾にIDがついていると思います。
そのIDごと引っ張ってきて、そのままCDKコードとして実装してしまう傾向にあるようです。

この辺りは願わくば良しなにやってくれると嬉しいですね。
```diff_typescript:cdk-stack.ts
    // Bedrock Agent用のロール
    const bedrockAgentRole = new iam.Role(this, 'BedrockAgentRole', {
      roleName: 'AmazonBedrockExecutionRoleForAgents_B0*********',
      (以下略)
      
    // DynamoDBからデータを取得するLambda関数用のロール
    const getDynamoDbRole = new iam.Role(this, 'GetDynamoDbRole', {
      roleName: 'get-data-from-dynamodb-role-6z******',
      (以下略)
```

# 実行してみる
完成したCDKプロジェクトを実際にデプロイしてみます。

```terminal:ターミナル
$ cdk deploy

Error: Resolution error: ID components may not include unresolved tokens: DeploymentStage.${Token[TOKEN.39]}.
```

エラーが出たので追加で修正依頼。
```diff_typescript:cdk-stack.ts
+   // 環境変数の値を取得（非トークン形式）
+   const envValue = environment.valueAsString;
    
    // API Gateway
    const api = new apigateway.RestApi(this, 'EvaluationApi', {
-     restApiName: `students-evaluation-api-${environment.valueAsString}-by-q`,
+     restApiName: `students-evaluation-api-${envValue}-by-q`,
      description: '学生評価システム用API',
-     deployOptions: {
-       stageName: environment.valueAsString,
-     },
    });
    
+   // 明示的にデプロイとステージを作成
+   const deployment = new apigateway.Deployment(this, 'Deployment', {
+     api,
+   });
+   
+   const stage = new apigateway.Stage(this, 'ApiStage', {
+     deployment,
+     stageName: envValue,
+   });
+   
+   // デフォルトステージをカスタムステージに設定
+   api.deploymentStage = stage;
```

再度デプロイすると、上手いことリソース生成できました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/dd0d6ba3-29fd-4029-80d4-6f79035b8c38.png)

# まとめ
多少修正が必要でしたが、良い感じに検証環境のリソースをCDKコード化することができました！

とはいえ、実はLambda関数の内容が少し違っていたり、Bedrock Agentのinstructionが簡略化されていたり、IAMロールの権限が不足していたり…という部分もありました。
なので出力されてはいオッケー、となることはまだなさそうです。

ですが冒頭でも述べた通り、0→1でCDKコードを作成するのに比べるとかなり手間が省けるのは良いと思います！
無料枠だけでも結構使えるみたいなので、ぜひ使っていきましょう〜

https://github.com/har1101/q-dev-cli-iac