GenU のバックエンド (CDK) 詳細解説　⑬DashBoard スタックの確認

## はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、[AWS GenU](https://aws-samples.github.io/generative-ai-use-cases-jp/) およびそれに必要なデータ基盤の探求 ([Snowflake](https://www.snowflake.com/ja/), [dbt](https://www.getdbt.com/), [Iceberg](https://iceberg.apache.org/), etc) に取り組む必要があると考えています。

本投稿では、[GenU のバックエンドである CDK コード](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main/packages/cdk)を詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。

## 前回までのおさらい

前回までで、以下が完了しました。

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)
- [⑥RagKnowledgeBaseStack スタックの解説](https://qiita.com/siruko/items/1223c9d22e73168a8809)
- [⑦WebSearchAgentStack スタックの解説](https://qiita.com/siruko/items/aef0a9599df60d47eb1e)
- [⑧GuardrailStack スタックの解説](https://qiita.com/siruko/items/d16e9fe27df4673d7554)
- [⑨GenerativeAiUseCasesStack > Auth スタックの解説](https://qiita.com/siruko/items/0c14040a1af132f42382)
- [⑩GenerativeAiUseCasesStack > Database, Api スタックの解説](https://qiita.com/siruko/items/5f249f11847f3829c18a)
- [⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの解説](https://qiita.com/siruko/items/1063daa2efe0b374a3fc)
- [⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの解説](https://qiita.com/siruko/items/73d6006004caf8648594)
  GenU の CDK は最大で以下の 6 つの子スタックを作成します。

- `CloudFrontWafStack`
- `RagKnowledgeBaseStack`
- `AgentStack`
- `GuardrailStack`
- `GenerativeAiUseCasesStack` ※メインスタック
- `DashboardStack`

今回は GenU 内の `DashboardStack` スタックを解説していきたいと思います。

### DashboardStack スタック

DashboardStack は CloudWatch ダッシュボードのスタックです。
アーキテクチャ図でいうと、以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/04d7c8e9-fb16-4f4c-823b-651f4203ff2b.png)

以下のソースコードが DashboardStack の定義です。

```typescript:packages/cdk/lib/create-stacks.ts (抜粋)
  const dashboardStack = params.dashboard
    ? new DashboardStack(
        app,
        `GenerativeAiUseCasesDashboardStack${params.env}`,
        {
          env: {
            account: params.account,
            region: params.modelRegion,
          },
          params: params,
          userPool: generativeAiUseCasesStack.userPool,
          userPoolClient: generativeAiUseCasesStack.userPoolClient,
          appRegion: params.region,
          crossRegionReferences: true,
        }
      )
    : null;
```

DashboardStack リソースの実体は [`packages/cdk/lib/dashboard-stack.ts`]() にあります。
スタック作成時のパラメータ `dashboard` (デフォルト値は false) が true の場合、以下のコードを実行し DashboardStack を作成します。

```typescript:packages/cdk/lib/dashboard-stack.ts
import { Stack, StackProps, CfnOutput, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cw from 'aws-cdk-lib/aws-cloudwatch';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import { StackInput } from './stack-input';

export interface DashboardStackProps extends StackProps {
  params: StackInput;
  userPool: cognito.UserPool;
  userPoolClient: cognito.UserPoolClient;
  appRegion: string;
}

export class DashboardStack extends Stack {
  public readonly logGroup: logs.LogGroup;
  public readonly dashboard: cw.Dashboard;

  constructor(scope: Construct, id: string, props: DashboardStackProps) {
    super(scope, id, props);

    const params = props.params;

    // Bedrock のログの出力先として設定する LogGroup
    const logGroup = new logs.LogGroup(this, 'LogGroup', {
      // 1 年でリテンションする設定
      retention: logs.RetentionDays.ONE_YEAR,
    });

    const inputTokenCounts = params.modelIds.map((modelId: string) => {
      return new cw.Metric({
        namespace: 'AWS/Bedrock',
        metricName: 'InputTokenCount',
        dimensionsMap: {
          ModelId: modelId,
        },
        period: Duration.days(1),
        statistic: 'Sum',
      });
    });

    const outputTokenCounts = params.modelIds.map((modelId: string) => {
      return new cw.Metric({
        namespace: 'AWS/Bedrock',
        metricName: 'OutputTokenCount',
        dimensionsMap: {
          ModelId: modelId,
        },
        period: Duration.days(1),
        statistic: 'Sum',
      });
    });

    const invocations = [
      ...params.modelIds,
      ...params.imageGenerationModelIds,
    ].map((modelId: string) => {
      return new cw.Metric({
        namespace: 'AWS/Bedrock',
        metricName: 'Invocations',
        dimensionsMap: {
          ModelId: modelId,
        },
        period: Duration.days(1),
        statistic: 'Sum',
      });
    });

    const userPoolMetrics = [
      'SignInSuccesses',
      'TokenRefreshSuccesses',
      'SignUpSuccesses',
    ].map((metricName: string) => {
      return new cw.Metric({
        namespace: 'AWS/Cognito',
        metricName,
        dimensionsMap: {
          UserPool: props.userPool.userPoolId,
          UserPoolClient: props.userPoolClient.userPoolClientId,
        },
        period: Duration.hours(1),
        statistic: 'Sum',
        region: props.appRegion,
      });
    });

    const dashboard = new cw.Dashboard(this, 'Dashboard', {
      defaultInterval: Duration.days(7),
    });

    dashboard.addWidgets(
      new cw.TextWidget({
        markdown: '**Amazon Bedrock Metrics**',
        width: 18,
        height: 1,
      }),
      new cw.TextWidget({
        markdown: '**User Metrics**',
        width: 6,
        height: 1,
      })
    );

    dashboard.addWidgets(
      new cw.GraphWidget({
        title: 'InputTokenCount (Daily)',
        width: 6,
        height: 6,
        left: inputTokenCounts,
      }),
      new cw.GraphWidget({
        title: 'OutputTokenCount (Daily)',
        width: 6,
        height: 6,
        left: outputTokenCounts,
      }),
      new cw.GraphWidget({
        title: 'Invocations (Daily)',
        width: 6,
        height: 6,
        left: invocations,
      }),
      new cw.GraphWidget({
        title: 'UserPool',
        width: 6,
        height: 6,
        left: userPoolMetrics,
      })
    );

    dashboard.addWidgets(
      new cw.TextWidget({
        markdown: '**Prompt Logs**',
        width: 24,
        height: 1,
      })
    );

    // ログの出力から抜き出す
    dashboard.addWidgets(
      new cw.LogQueryWidget({
        title: 'Prompt Logs',
        width: 24,
        height: 6,
        logGroupNames: [logGroup.logGroupName],
        view: cw.LogQueryVisualizationType.TABLE,
        queryLines: [
          "filter @logStream = 'aws/bedrock/modelinvocations'",
          "filter schemaType like 'ModelInvocationLog'",
          'filter concat(input.inputBodyJson.prompt, input.inputBodyJson.messages.0.content.0.text) not like /.*<conversation>.*/',
          'sort @timestamp desc',
          'fields @timestamp, concat(input.inputBodyJson.prompt, input.inputBodyJson.messages.0.content.0.text) as input, modelId',
        ],
      })
    );

    this.logGroup = logGroup;
    this.dashboard = dashboard;

    new CfnOutput(this, 'BedrockLogGroup', {
      value: this.logGroup.logGroupName,
    });

    new CfnOutput(this, 'DashboardName', {
      value: this.dashboard.dashboardName,
    });

    new CfnOutput(this, 'DashboardUrl', {
      value: `https://console.aws.amazon.com/cloudwatch/home#dashboards/dashboard/${this.dashboard.dashboardName}`,
    });
  }
}
```

この中では、CloudWatch ダッシュボードおよびダッシュボードに表示するロググループ、メトリクスを生成しています。

#### DashboardStack > LogGroup リソース

LogGroup は CloudWatch ロググループ です。
以下のソースコードが LogGroup の定義です。

```typescript:packages/cdk/lib/dashboard-stack.ts (抜粋)
    // Bedrock のログの出力先として設定する LogGroup
    const logGroup = new logs.LogGroup(this, 'LogGroup', {
      // 1 年でリテンションする設定
      retention: logs.RetentionDays.ONE_YEAR,
    });
```

ここでは、以下の CloudWatch ロググループ を生成しています。

- [`CloudWatch ロググループ`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroup.html)
  - ログの保持期間: 1 年間 (365 日)

#### DashboardStack > Metric リソース

Metric は CloudWatch メトリクス です。
以下のソースコードが Metric の定義です。

```typescript:packages/cdk/lib/dashboard-stack.ts (抜粋)
    const inputTokenCounts = params.modelIds.map((modelId: string) => {
      return new cw.Metric({
        namespace: 'AWS/Bedrock',
        metricName: 'InputTokenCount',
        dimensionsMap: {
          ModelId: modelId,
        },
        period: Duration.days(1),
        statistic: 'Sum',
      });
    });

    const outputTokenCounts = params.modelIds.map((modelId: string) => {
      return new cw.Metric({
        namespace: 'AWS/Bedrock',
        metricName: 'OutputTokenCount',
        dimensionsMap: {
          ModelId: modelId,
        },
        period: Duration.days(1),
        statistic: 'Sum',
      });
    });

    const invocations = [
      ...params.modelIds,
      ...params.imageGenerationModelIds,
    ].map((modelId: string) => {
      return new cw.Metric({
        namespace: 'AWS/Bedrock',
        metricName: 'Invocations',
        dimensionsMap: {
          ModelId: modelId,
        },
        period: Duration.days(1),
        statistic: 'Sum',
      });
    });

    const userPoolMetrics = [
      'SignInSuccesses',
      'TokenRefreshSuccesses',
      'SignUpSuccesses',
    ].map((metricName: string) => {
      return new cw.Metric({
        namespace: 'AWS/Cognito',
        metricName,
        dimensionsMap: {
          UserPool: props.userPool.userPoolId,
          UserPoolClient: props.userPoolClient.userPoolClientId,
        },
        period: Duration.hours(1),
        statistic: 'Sum',
        region: props.appRegion,
      });
    });
```

ここでは、以下の 6 つのメトリクスを生成しています。

- `InputTokenCount` メトリクス
  - スタック作成時のパラメータ `modelIds` (デフォルト値は以下) の数だけ、[メトリクス](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Metric.html)を作成
    - `us.anthropic.claude-3-5-sonnet-20241022-v2:0`
    - `us.anthropic.claude-3-5-haiku-20241022-v1:0`
    - `us.amazon.nova-pro-v1:0`
    - `us.amazon.nova-lite-v1:0`
    - `us.amazon.nova-micro-v1:0`
  - 名前空間: `'AWS/Bedrock'`
  - メトリクス: 入力テキストのトークン数
  - モデル ID: 対象のモデルのモデル ID
  - 期間: 1 日
  - 統計: 合計値
- `OutputTokenCount` メトリクス
  - スタック作成時のパラメータ `modelIds` の数だけ、[メトリクス](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Metric.html)を作成
  - 名前空間: `'AWS/Bedrock'`
  - メトリクス: 出力テキストのトークン数
  - モデル ID: 対象のモデルのモデル ID
  - 期間: 1 日
  - 統計: 合計値
- `Invocations` メトリクス
  - スタック作成時のパラメータ `modelIds` の数だけ、[メトリクス](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Metric.html)を作成
  - 名前空間: `'AWS/Bedrock'`
  - メトリクス: InvokeModel もしくは InvokeModelWithResponseStream の API 操作によるリクエストの数
  - モデル ID: 対象のモデルのモデル ID
  - 期間: 1 日
  - 統計: 合計値
- `SignInSuccesses` メトリクス
  - 名前空間: `'AWS/Cognito'`
  - メトリクス: Cognito ユーザープールに対して正常に行われたユーザー認証リクエストの合計数
  - ユーザプール ID: [`Auth`](https://qiita.com/siruko/items/0c14040a1af132f42382#generativeaiusecasesstack--auth-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) リソースの Cognito ユーザプール ID
  - ユーザプールクライアント: [`Auth`](https://qiita.com/siruko/items/0c14040a1af132f42382#generativeaiusecasesstack--auth-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) リソースの Cognito アプリケーションクライアント ID
  - 期間: 1 時間
  - 統計: 合計値
  - リージョン: スタック作成時のパラメータ `region` (デフォルト値は以下のいずれか)
    - 環境変数の `CDK_DEFAULT_REGION`
    - 固定値 `'us-east-1'`
- `TokenRefreshSuccesses` メトリクス
  - 名前空間: `'AWS/Cognito'`
  - メトリクス: Cognito ユーザープールに対する更新リクエストのうち、正常に行われたものの合計数
  - ユーザプール ID: [`Auth`](https://qiita.com/siruko/items/0c14040a1af132f42382#generativeaiusecasesstack--auth-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) リソースの Cognito ユーザプール ID
  - ユーザプールクライアント: [`Auth`](https://qiita.com/siruko/items/0c14040a1af132f42382#generativeaiusecasesstack--auth-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) リソースの Cognito アプリケーションクライアント ID
  - 期間: 1 時間
  - 統計: 合計値
  - リージョン: スタック作成時のパラメータ `region` (デフォルト値は以下のいずれか)
    - 環境変数の `CDK_DEFAULT_REGION`
    - 固定値 `'us-east-1'`
- `SignUpSuccesses` メトリクス
  - 名前空間: `'AWS/Cognito'`
  - メトリクス: Cognito ユーザープールに対して正常に行われたユーザー登録リクエストの合計数
  - ユーザプール ID: [`Auth`](https://qiita.com/siruko/items/0c14040a1af132f42382#generativeaiusecasesstack--auth-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) リソースの Cognito ユーザプール ID
  - ユーザプールクライアント: [`Auth`](https://qiita.com/siruko/items/0c14040a1af132f42382#generativeaiusecasesstack--auth-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9) リソースの Cognito アプリケーションクライアント ID
  - 期間: 1 時間
  - 統計: 合計値
  - リージョン: スタック作成時のパラメータ `region` (デフォルト値は以下のいずれか)
    - 環境変数の `CDK_DEFAULT_REGION`
    - 固定値 `'us-east-1'`

#### DashboardStack > Dashboard リソース

Dashboard は CloudWatch ダッシュボード です。
以下のソースコードが Dashboard の定義です。

```typescript:packages/cdk/lib/dashboard-stack.ts (抜粋)
    const dashboard = new cw.Dashboard(this, 'Dashboard', {
      defaultInterval: Duration.days(7),
    });

    dashboard.addWidgets(
      new cw.TextWidget({
        markdown: '**Amazon Bedrock Metrics**',
        width: 18,
        height: 1,
      }),
      new cw.TextWidget({
        markdown: '**User Metrics**',
        width: 6,
        height: 1,
      })
    );

    dashboard.addWidgets(
      new cw.GraphWidget({
        title: 'InputTokenCount (Daily)',
        width: 6,
        height: 6,
        left: inputTokenCounts,
      }),
      new cw.GraphWidget({
        title: 'OutputTokenCount (Daily)',
        width: 6,
        height: 6,
        left: outputTokenCounts,
      }),
      new cw.GraphWidget({
        title: 'Invocations (Daily)',
        width: 6,
        height: 6,
        left: invocations,
      }),
      new cw.GraphWidget({
        title: 'UserPool',
        width: 6,
        height: 6,
        left: userPoolMetrics,
      })
    );

    dashboard.addWidgets(
      new cw.TextWidget({
        markdown: '**Prompt Logs**',
        width: 24,
        height: 1,
      })
    );

    // ログの出力から抜き出す
    dashboard.addWidgets(
      new cw.LogQueryWidget({
        title: 'Prompt Logs',
        width: 24,
        height: 6,
        logGroupNames: [logGroup.logGroupName],
        view: cw.LogQueryVisualizationType.TABLE,
        queryLines: [
          "filter @logStream = 'aws/bedrock/modelinvocations'",
          "filter schemaType like 'ModelInvocationLog'",
          'filter concat(input.inputBodyJson.prompt, input.inputBodyJson.messages.0.content.0.text) not like /.*<conversation>.*/',
          'sort @timestamp desc',
          'fields @timestamp, concat(input.inputBodyJson.prompt, input.inputBodyJson.messages.0.content.0.text) as input, modelId',
        ],
      })
    );
```

ここでは、以下の CloudWatch ダッシュボードおよびウィジェットを生成しています。

- [`CloudWatch ダッシュボード`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Dashboard.html)
  - メトリクスの表示期間: 過去 7 日間
  - 上から以下のウィジェットを追加
    - [`テキストウィジェット`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.TextWidget.html)
      - 幅 3 で `'Amazon Bedrock Metrics'`
      - 幅 1 で `'User Metrics'`
    - [`グラフウィジェット`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.GraphWidget.html)
      - 幅 1 で `InputTokenCount` メトリクス
      - 幅 1 で `OutputTokenCount` メトリクス
      - 幅 1 で `Invocations` メトリクス
      - 幅 1 で `UserPool` メトリクス (`SignInSuccesses` メトリクス, `TokenRefreshSuccesses` メトリクス, `SignUpSuccesses` メトリクスを 1 つのグラフに表示)
    - [`テキストウィジェット`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.TextWidget.html)
      - 幅 4 で `'Prompt Logs'`
    - [`ログクエリウィジェット`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.LogQueryWidget.html)
      - 幅 4
      - タイトル: `'Prompt Logs'`
      - ロググループ名: DashboardStack > LogGroup リソースで生成した CloudWatch ロググループ
      - ビュー: [テーブルビュー](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.LogQueryVisualizationType.html#table)
      - クエリ:
        - `filter @logStream = 'aws/bedrock/modelinvocations'`
        - `filter schemaType like 'ModelInvocationLog'`
        - `filter concat(input.inputBodyJson.prompt, input.inputBodyJson.messages.0.content.0.text) not like /.*<conversation>.*/`
        - `sort @timestamp desc`
        - `fields @timestamp, concat(input.inputBodyJson.prompt, input.inputBodyJson.messages.0.content.0.text) as input, modelId`
      - クエリ解説
        - Bedrock モデルの推論ログを最新のものから表示する
        - `<conversation>` を含むログは表示しない
        - 推論時間、インプット内容、モデル ID を表示する

これにて、GenU 内の全てのスタックの解説が終わりました。
次回はおまけとしてスタックからエクスポート (CfnOutput) される値を解説したいと思います。

## (参考) GenU のバックエンド (CDK) 詳細解説投稿一覧

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)
- [⑥RagKnowledgeBaseStack スタックの解説](https://qiita.com/siruko/items/1223c9d22e73168a8809)
- [⑦WebSearchAgentStack スタックの解説](https://qiita.com/siruko/items/aef0a9599df60d47eb1e)
- [⑧GuardrailStack スタックの解説](https://qiita.com/siruko/items/d16e9fe27df4673d7554)
- [⑨GenerativeAiUseCasesStack > Auth スタックの解説](https://qiita.com/siruko/items/0c14040a1af132f42382)
- [⑩GenerativeAiUseCasesStack > Database, Api スタックの解説](https://qiita.com/siruko/items/5f249f11847f3829c18a)
- [⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの解説](https://qiita.com/siruko/items/1063daa2efe0b374a3fc)
- [⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの解説](https://qiita.com/siruko/items/73d6006004caf8648594)
- [⑬DashBoard スタックの解説](https://qiita.com/siruko/items/73a75c7d0146d12ecbc3)
- [⑭GenU の Outputs の解説](https://qiita.com/siruko/items/afc14128a5b1a15ab69c)