13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

何を監視すべきか迷わないために - サーバレス監視設計パターン

13
Posted at

はじめに

これまで、「何を監視すべきか迷わないために」シリーズとして、以下の2つのアーキテクチャパターンにおける監視設計を解説してきました。

  • 第1弾: ALB + EC2 + RDS(インスタンスレベル監視)
  • 第2弾: ALB + ECS Fargate + RDS(タスクレベル監視)

今回は第3弾として、API Gateway + Lambda + DynamoDBというサーバレスアーキテクチャの監視設計について解説します。

サーバレスアーキテクチャは、インフラ管理の負担を大幅に軽減できる一方で、従来型やコンテナ構成とは異なる監視の考え方が必要です。EC2やFargateでは「CPU使用率」「メモリ使用率」を監視していましたが、Lambdaでは**関数の実行時間(Duration)スロットリング(Throttles)**を監視します。また、RDSの代わりに使用するDynamoDBでは、キャパシティユニットの消費という全く異なる概念の監視が求められます。

さらに、サーバレス特有の課題として、以下のような問題があります。

  • コールドスタート問題: Lambda関数の初回実行や長時間未使用後の実行が遅くなる
  • 同時実行数制限: アカウントレベルの制限(デフォルト1000)により、リクエストが拒否される可能性
  • タイムアウト制限: 最大15分という実行時間の制約
  • DynamoDBのスロットリング: キャパシティを超えるとリクエストが拒否される

本記事では、これらの課題に対処するための監視設計を、Four Golden SignalsとAWS公式のベストプラクティスに基づいて解説します。また、AWS CDK(TypeScript)を使用した実装例も紹介します。

まずは結論

サーバレス監視の重要ポイント

  1. コールドスタート: CloudWatch Logs InsightsとX-RayでInit Durationを分析。必要に応じてプロビジョンド同時実行数を設定
  2. Throttling: Lambda/DynamoDB両方で監視。1件でも発生したら即アラート・対策
  3. 同時実行数: アカウント制限の70%でアラート。Throttles発生前に対策
  4. Duration: タイムアウトとコストに直結。70%で警告、90%で緊急アラート
  5. キャパシティ: DynamoDBのモード選択とスロットリング監視が重要
  6. X-Rayによる分散トレーシング: サービス間のレイテンシとボトルネックを可視化。パフォーマンス分析に非常に有効

構成とEC2/Fargate構成との違い

アーキテクチャ比較

まず、3つのアーキテクチャを比較してみましょう。

EC2構成(第1弾):

Internet → [ALB] → [EC2インスタンス] → [RDS]
              ↓
        インスタンスレベルの監視
        CPU/メモリ使用率

Fargate構成(第2弾):

Internet → [ALB] → [ECS Fargate タスク] → [RDS]
              ↓
        タスクレベルの監視
        タスク単位のCPU/メモリ

サーバレス構成(今回):

Internet → [API Gateway] → [Lambda関数] → [DynamoDB]
                  ↓
        関数実行レベルの監視
        Duration/Errors/Throttles

監視における主要な違い

観点 EC2構成 Fargate構成 サーバレス構成
監視単位 EC2インスタンス ECSタスク Lambda関数実行
リソース管理 インスタンスタイプ タスク定義 メモリ設定のみ(CPUは自動)
スケーリング インスタンス数 タスク数 自動(同時実行数)
主要メトリクス CPU/メモリ CPU/メモリ/タスク数 Duration/Errors/Throttles
インフラ管理 OS管理必要 マネージド 完全マネージド
課金 時間課金 時間課金 実行時間課金
ログ収集 Agent設定 タスク定義 自動(CloudWatch Logs)
データベース RDS(接続数監視) RDS(接続数監視) DynamoDB(キャパシティ監視)

サーバレス構成特有の監視課題

1. コールドスタート問題

Lambda関数は、初回実行時や長時間未使用後の実行時に、実行環境の初期化(コールドスタート)が発生します。これにより、通常の実行時間よりも大幅に遅くなることがあります。

対策:

  • プロビジョンド同時実行数の設定
  • Lambda SnapStart(Javaのみ)の活用
  • 関数のメモリサイズ増加

2. 同時実行数制限

Lambdaには、アカウントレベルでデフォルト1000という同時実行数の制限があります。この制限に達すると、新しいリクエストが**Throttle(スロットリング)**され、エラーとなります。

対策:

  • 予約済み同時実行数の設定
  • アカウント制限の引き上げ申請
  • 関数のDuration短縮

3. タイムアウト制限

Lambdaの最大実行時間は15分です。長時間処理が必要な場合は、Step Functionsへの分割などの設計変更が必要です。

4. DynamoDBのキャパシティ管理

DynamoDBでは、プロビジョンドモードの場合、設定したキャパシティを超えるとスロットリングが発生します。オンデマンドモードでも、急激なトラフィック増加には対応できない場合があります。

監視設計の基本原則

前回と同様、Four Golden Signalsを基本原則とします。

  1. Latency(レイテンシ): リクエストの処理時間
  2. Traffic(トラフィック): システムへのリクエスト量
  3. Errors(エラー): 失敗したリクエストの割合
  4. Saturation(飽和度): リソースの使用状況

ただし、サーバレス構成では以下の追加要素を考慮する必要があります。

  • 関数実行単位の監視: インスタンス/タスクではなく、関数の呼び出しごとに監視
  • 自動スケーリング: 明示的なスケーリング設定は不要だが、同時実行数制限に注意
  • コスト: 実行時間課金のため、Duration監視がコスト管理に直結
  • コールドスタート: 初期化時間(Init Duration)の監視が重要

これらを踏まえて、AWS公式のベストプラクティスと組み合わせた監視設計を構築します。

各レイヤーの監視設計

API Gateway(エントリーポイント層)

API Gatewayは、サーバレスアーキテクチャのエントリーポイントです。前回のALBと同様の役割ですが、メトリクスは異なります。

必須メトリクス

1. Latency(エンドツーエンドレイテンシ)

何を測定するか:
API Gatewayがクライアントからリクエストを受け取ってから、クライアントにレスポンスを返すまでの全体の時間を測定します。

なぜ重要か:
ユーザー体験に直結する最も重要な指標です(Latency)。この値が高い場合、ユーザーは「遅いサービス」と感じます。

EC2/Fargate構成との違い:

  • ALB: TargetResponseTime(ALBからターゲットまでの時間)
  • API Gateway: Latency(エンドツーエンド全体)

API GatewayのLatencyには、Lambda実行時間、DynamoDBアクセス時間、ネットワーク遅延などすべてが含まれます。

しきい値の考え方:

  • 警告: 1000ms以上(平均)
  • 緊急: 3000ms以上(平均)

RESTful APIの場合、1秒以内のレスポンスが望ましいです。

CDKコード例:

const latencyAlarm = new cloudwatch.Alarm(this, 'ApiGatewayHighLatency', {
  metric: restApi.metricLatency({
    statistic: 'Average',
    period: cdk.Duration.minutes(5),
  }),
  threshold: 1000,
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'API Gatewayのレイテンシが高い',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
2. IntegrationLatency(統合レイテンシ)

何を測定するか:
API GatewayがバックエンドのLambda関数にリクエストを送信してから、レスポンスを受け取るまでの時間を測定します。

なぜ重要か:
これはサーバレス構成特有のメトリクスです。LatencyIntegrationLatencyを比較することで、「API Gateway自体の処理時間」と「Lambda実行時間」を分離して把握できます。

例えば、Latencyが3秒でIntegrationLatencyが2.8秒の場合、ほとんどの時間がLambda実行に費やされていることがわかります。

EC2/Fargate構成との違い:
ALBには対応する概念がありません。サーバレスでは、Lambda実行時間を分離して監視できる点が大きな利点です。

しきい値の考え方:

  • 警告: 800ms以上(平均)
  • 緊急: 2500ms以上(平均)

Latencyよりも少し低めに設定し、Lambda実行時間の問題を早期に検知します。

CDKコード例:

const integrationLatencyAlarm = new cloudwatch.Alarm(this, 'ApiGatewayHighIntegrationLatency', {
  metric: restApi.metricIntegrationLatency({
    statistic: 'Average',
    period: cdk.Duration.minutes(5),
  }),
  threshold: 800,
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'Lambda統合レイテンシが高い',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
3. 4XXError(クライアントエラー)

何を測定するか:
クライアント側の問題によるエラー(認証エラー、不正なリクエスト形式など)の数を測定します。

なぜ重要か:
クライアント側の問題を検知し、API設計やドキュメントの改善に役立てることができます(Errors)。

EC2/Fargate構成との違い:
ALBのHTTPCode_Target_4XX_Countと同じ概念ですが、API Gatewayはより詳細なエラー情報を提供します。

しきい値の考え方:

  • 警告: 全リクエストの5%以上
  • 緊急: 全リクエストの20%以上

4XXエラーは通常クライアント側の問題ですが、急増する場合はAPI仕様変更やドキュメント不備の可能性があります。

CDKコード例:

const clientErrorAlarm = new cloudwatch.Alarm(this, 'ApiGateway4XXError', {
  metric: restApi.metricClientError({
    statistic: 'Sum',
    period: cdk.Duration.minutes(5),
  }),
  threshold: 50, // 5分間で50件以上
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'API Gateway 4XXエラーが多発',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
4. 5XXError(サーバーエラー)

何を測定するか:
サーバー側の問題によるエラー(Lambdaのエラー、タイムアウト、API Gateway自体の問題など)の数を測定します。

なぜ重要か:
サービスの可用性に直結する最も重要なエラー指標です(Errors)。5XXエラーは、ユーザーがサービスを利用できない状態を意味します。

EC2/Fargate構成との違い:
ALBのHTTPCode_Target_5XX_Countと同じ概念ですが、サーバレスではLambdaのエラーやタイムアウトが5XXエラーとして返されます。

しきい値の考え方:

  • 警告: 1分間に5件以上
  • 緊急: 1分間に20件以上

5XXエラーは即座に対応が必要です。

CDKコード例:

const serverErrorAlarm = new cloudwatch.Alarm(this, 'ApiGateway5XXError', {
  metric: restApi.metricServerError({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: 5,
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'API Gateway 5XXエラーが発生',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
5. Count(リクエスト数)

何を測定するか:
API Gatewayが処理したリクエストの総数を測定します。

なぜ重要か:
トラフィックの傾向を把握し、スケーリングやキャパシティプランニングに役立てることができます(Traffic)。

EC2/Fargate構成との違い:
ALBのRequestCountと同じ概念です。サーバレスでは、この値から同時実行数制限への影響を予測できます。

しきい値の考え方:
リクエスト数自体にアラートを設定するよりも、ダッシュボードで可視化し、トレンドを把握することが重要です。

CDKコード例:

// ダッシュボードでの可視化が主目的
const requestCountMetric = restApi.metricCount({
  statistic: 'Sum',
  period: cdk.Duration.minutes(1),
});

Lambda(関数実行層)

Lambdaの監視は、今回のメインコンテンツです。EC2/Fargate構成とは全く異なる考え方が必要です。

必須メトリクス

1. Duration(実行時間)

何を測定するか:
Lambda関数の実行時間をミリ秒単位で測定します。これは、関数のコードが実行開始から終了までにかかった時間です。

なぜ重要か:
Durationは以下の3つの理由で極めて重要です。

  1. レイテンシに直結Latency): Durationが長いと、API GatewayのIntegrationLatencyも長くなり、最終的にユーザーのレスポンス時間が悪化します。
  2. コストに直結: Lambdaは「実行時間 × メモリサイズ」で課金されます。Durationが長いほど、コストも高くなります。
  3. タイムアウトリスク: Lambda関数には設定されたタイムアウト値があります(デフォルト3秒、最大15分)。Durationがタイムアウトに近づくと、関数が強制終了されるリスクがあります。

EC2/Fargate構成との違い:

  • EC2/Fargate: CPU使用率を監視し、CPU使用率が高い場合は処理が遅いと判断
  • Lambda: 実行時間を直接監視し、タイムアウトやコストに影響

Lambdaでは、CPU使用率という概念はありません(メモリサイズに応じてCPUが自動的に割り当てられる)。代わりに、実行時間を直接監視します。

しきい値の考え方:

  • 警告: タイムアウト設定の70%以上
  • 緊急: タイムアウト設定の90%以上

例えば、タイムアウトが10秒の場合、7秒で警告、9秒で緊急アラートを設定します。

CDKコード例:

// Lambda関数のタイムアウトを取得
const timeoutWarning = lambdaFunction.timeout.toMilliseconds() * 0.7;
const timeoutCritical = lambdaFunction.timeout.toMilliseconds() * 0.9;

// Duration警告アラーム
const durationWarningAlarm = new cloudwatch.Alarm(this, 'LambdaDurationWarning', {
  metric: lambdaFunction.metricDuration({
    statistic: 'Average',
    period: cdk.Duration.minutes(5),
  }),
  threshold: timeoutWarning,
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: `Lambda実行時間がタイムアウトの70%を超えています`,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

// Duration緊急アラーム
const durationCriticalAlarm = new cloudwatch.Alarm(this, 'LambdaDurationCritical', {
  metric: lambdaFunction.metricDuration({
    statistic: 'Average',
    period: cdk.Duration.minutes(5),
  }),
  threshold: timeoutCritical,
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: `Lambda実行時間がタイムアウトの90%を超えています`,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
2. Errors(エラー数)

何を測定するか:
Lambda関数の実行中に発生したエラー(例外、タイムアウト、メモリ不足など)の数を測定します。

なぜ重要か:
エラーは、サービスの可用性に直結します(Errors)。エラーが発生すると、API Gatewayは5XXエラーを返し、ユーザーはサービスを利用できません。

EC2/Fargate構成との違い:

  • EC2/Fargate: アプリケーションレベルの5xxエラーをALBで監視
  • Lambda: 関数レベルのエラーを直接監視

Lambdaでは、関数が例外をスローした場合や、タイムアウトした場合に、自動的にErrorsメトリクスがインクリメントされます。

しきい値の考え方:

  • 警告: 1分間に5件以上
  • 緊急: 1分間に20件以上

エラー率(Errors / Invocations)も監視すると良いでしょう。例えば、エラー率が5%を超えたら警告、20%を超えたら緊急とします。

CDKコード例:

// エラー数アラーム
const errorAlarm = new cloudwatch.Alarm(this, 'LambdaErrors', {
  metric: lambdaFunction.metricErrors({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: 5,
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'Lambda関数でエラーが多発しています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

// エラー率アラーム(オプション)
const errorRateMetric = new cloudwatch.MathExpression({
  expression: '(errors / invocations) * 100',
  usingMetrics: {
    errors: lambdaFunction.metricErrors({
      statistic: 'Sum',
      period: cdk.Duration.minutes(5),
    }),
    invocations: lambdaFunction.metricInvocations({
      statistic: 'Sum',
      period: cdk.Duration.minutes(5),
    }),
  },
});

const errorRateAlarm = new cloudwatch.Alarm(this, 'LambdaErrorRate', {
  metric: errorRateMetric,
  threshold: 5, // 5%
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'Lambdaエラー率が5%を超えています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
3. Throttles(スロットリング)

何を測定するか:
同時実行数制限により拒否されたリクエストの数を測定します。

なぜ重要か:
Throttlesは、サーバレス特有の極めて重要なメトリクスです。EC2/Fargateにはこの概念はありません。

Lambdaには、アカウントレベルでデフォルト1000という同時実行数の制限があります。この制限に達すると、新しいリクエストはThrottle(スロットリング)され、エラーとなります。Throttlesが発生すると、ユーザーはサービスを利用できず、可用性が低下します(Errors)。

EC2/Fargate構成との違い:

  • EC2/Fargate: インスタンス/タスク数を手動または自動でスケーリング。制限はあるが、通常は事前に設定
  • Lambda: 自動スケーリングだが、アカウントレベルの制限が存在。制限に達すると即座にスロットリング

しきい値の考え方:

  • 警告・緊急とも: 1件でも発生したら即アラート

Throttlesは絶対に発生させてはいけません。1件でも発生したら、即座に以下の対策を実施します。

  • 予約済み同時実行数の増加
  • アカウント制限の引き上げ申請
  • Lambda関数のDuration短縮(同時実行数を減らす)

CDKコード例:

const throttleAlarm = new cloudwatch.Alarm(this, 'LambdaThrottles', {
  metric: lambdaFunction.metricThrottles({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: 1, // 1件でもアラート
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'Lambda関数がスロットリングされています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
4. ConcurrentExecutions(同時実行数)

何を測定するか:
同時に実行されているLambda関数の数を測定します。

なぜ重要か:
ConcurrentExecutionsは、Throttlesの予兆を検知するために重要です(Saturation)。

アカウントレベルの制限(デフォルト1000)に達する前に警告を出すことで、Throttlesを未然に防ぐことができます。

EC2/Fargate構成との違い:

  • EC2/Fargate: 実行中のインスタンス数/タスク数を監視
  • Lambda: 同時実行数を監視(自動スケーリングの結果)

しきい値の考え方:

  • 警告: アカウント制限の70%以上(700同時実行)
  • 緊急: アカウント制限の90%以上(900同時実行)

CDKコード例:

// アカウントレベルの同時実行数を監視
const concurrentExecutionsAlarm = new cloudwatch.Alarm(this, 'LambdaHighConcurrency', {
  metric: new cloudwatch.Metric({
    namespace: 'AWS/Lambda',
    metricName: 'ConcurrentExecutions',
    dimensionsMap: {
      FunctionName: lambdaFunction.functionName,
    },
    statistic: 'Maximum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: 700, // アカウント制限1000の70%
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'Lambda同時実行数がアカウント制限の70%を超えています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
5. Invocations(呼び出し数)

何を測定するか:
Lambda関数が呼び出された回数を測定します。

なぜ重要か:
トラフィックの傾向を把握し、キャパシティプランニングに役立てることができます(Traffic)。

EC2/Fargate構成との違い:
ALBのRequestCountと同じ概念です。

しきい値の考え方:
通常、アラートは設定せず、ダッシュボードで可視化します。

CDKコード例:

// ダッシュボードでの可視化が主目的
const invocationsMetric = lambdaFunction.metricInvocations({
  statistic: 'Sum',
  period: cdk.Duration.minutes(1),
});
6. コールドスタート(Init Duration)の監視

何を測定するか:
Lambda関数の初期化時間(コールドスタート時のみ)を測定します。

なぜ重要か:
コールドスタートは、サーバレス特有の課題です。Lambda関数は、初回実行時や長時間未使用後の実行時に、実行環境の初期化が発生します。この初期化には数百ミリ秒から数秒かかることがあり、ユーザー体験に大きく影響します(Latency)。

EC2/Fargate構成との違い:
EC2/Fargateにはコールドスタートという概念はありません。インスタンス/タスクは常に起動しています。

しきい値の考え方:

  • 警告: 平均3000ms以上(CloudWatch Logs Insightsで分析)
  • 緊急: 平均5000ms以上

対策:

  • プロビジョンド同時実行数: 指定した数の実行環境を常時ウォーム状態に保つ(コスト増加)
  • Lambda SnapStart: Javaランタイムのみ対応。起動時間を最大10倍短縮
  • メモリサイズ増加: CPUも比例して増加するため、初期化が速くなる

CDKコード例:

CloudWatch MetricsにはInitDurationがないため、CloudWatch Logs Insightsで分析します。

// CloudWatch Logs Insightsクエリ(コールドスタート分析)
const coldStartQuery = `
fields @timestamp, @initDuration, @duration
| filter ispresent(@initDuration)
| stats avg(@initDuration) as avgInitDuration,
        max(@initDuration) as maxInitDuration,
        count() as coldStartCount
`;

// プロビジョンド同時実行数の設定例
const functionVersion = new lambda.Version(this, 'FunctionVersion', {
  lambda: lambdaFunction,
});

const alias = new lambda.Alias(this, 'FunctionAlias', {
  aliasName: 'prod',
  version: functionVersion,
  provisionedConcurrentExecutions: 10, // コールドスタート対策
});

DynamoDB(データベース層)

DynamoDBの監視は、RDSとは全く異なります。RDSでは接続数やクエリレイテンシを監視しましたが、DynamoDBではキャパシティユニットの消費を監視します。

必須メトリクス

1. ConsumedReadCapacityUnits(読み取りキャパシティ消費)

何を測定するか:
実際に消費された読み取りキャパシティユニット(RCU: Read Capacity Units)の数を測定します。

なぜ重要か:
プロビジョンドモードの場合、設定したRCUを超えるとスロットリングが発生し、リクエストが拒否されます(Saturation)。また、キャパシティ消費はコストに直結します。

EC2/Fargate構成との違い:

  • RDS: 接続数、CPU使用率、クエリレイテンシを監視
  • DynamoDB: キャパシティユニット消費を監視(RDSとは全く異なる概念)

DynamoDBでは、「1RCU = 4KBまでの強整合性読み取り1回」または「2回の結果整合性読み取り」です。

しきい値の考え方(プロビジョンドモードの場合):

  • 警告: プロビジョンドキャパシティの80%以上
  • 緊急: プロビジョンドキャパシティの95%以上

オンデマンドモードの場合、この監視は不要です(自動スケーリング)。

CDKコード例:

// プロビジョンドモードの場合
const readCapacityAlarm = new cloudwatch.Alarm(this, 'DynamoDBHighReadCapacity', {
  metric: table.metricConsumedReadCapacityUnits({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: table.readCapacity ? table.readCapacity * 60 * 0.8 : 240, // 80%
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'DynamoDB読み取りキャパシティが80%を超えています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
2. ConsumedWriteCapacityUnits(書き込みキャパシティ消費)

何を測定するか:
実際に消費された書き込みキャパシティユニット(WCU: Write Capacity Units)の数を測定します。

なぜ重要か:
読み取りと同様、設定したWCUを超えるとスロットリングが発生します(Saturation)。

「1WCU = 1KBまでの書き込み1回」です。

しきい値の考え方(プロビジョンドモードの場合):

  • 警告: プロビジョンドキャパシティの80%以上
  • 緊急: プロビジョンドキャパシティの95%以上

CDKコード例:

// プロビジョンドモードの場合
const writeCapacityAlarm = new cloudwatch.Alarm(this, 'DynamoDBHighWriteCapacity', {
  metric: table.metricConsumedWriteCapacityUnits({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: table.writeCapacity ? table.writeCapacity * 60 * 0.8 : 240, // 80%
  evaluationPeriods: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'DynamoDB書き込みキャパシティが80%を超えています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
3. UserErrors(ユーザーエラー)

何を測定するか:
アプリケーション起因のエラー(条件チェック失敗、ValidationException、ResourceNotFoundなど)の数を測定します。

なぜ重要か:
アプリケーションロジックの問題を検知できます(Errors)。ただし、これらはDynamoDB側の問題ではなく、アプリケーション側の問題です。

EC2/Fargate構成との違い:
RDSではクエリエラーをアプリケーション側で検知しますが、DynamoDBはUserErrorsメトリクスとして自動的に記録します。

しきい値の考え方:

  • 警告: 5分間に10件以上
  • 緊急: 5分間に50件以上

CDKコード例:

const userErrorsAlarm = new cloudwatch.Alarm(this, 'DynamoDBUserErrors', {
  metric: table.metricUserErrors({
    statistic: 'Sum',
    period: cdk.Duration.minutes(5),
  }),
  threshold: 10,
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'DynamoDBユーザーエラーが発生しています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
4. SystemErrors(システムエラー)

何を測定するか:
DynamoDB側の内部サーバーエラー(InternalServerError、ServiceUnavailableなど)の数を測定します。

なぜ重要か:
DynamoDBの可用性問題を検知できます(Errors)。これは非常に稀ですが、発生した場合は即座に対応が必要です。

EC2/Fargate構成との違い:
RDSでもデータベース側のエラーは発生しますが、DynamoDBはSystemErrorsメトリクスとして明示的に記録します。

しきい値の考え方:

  • 警告・緊急とも: 1件でも発生したら即アラート

SystemErrorsは、DynamoDB側の問題なので、AWSサポートへの連絡が必要な場合があります。

CDKコード例:

const systemErrorsAlarm = new cloudwatch.Alarm(this, 'DynamoDBSystemErrors', {
  metric: table.metricSystemErrors({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: 1, // 1件でもアラート
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'DynamoDBシステムエラーが発生しています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
5. ThrottledRequests(スロットリングされたリクエスト)

何を測定するか:
キャパシティ超過により拒否されたリクエストの数を測定します。

なぜ重要か:
ThrottledRequestsは、サーバレス特有の極めて重要なメトリクスです(Lambda Throttlesと同様)。

プロビジョンドモードでキャパシティを超えた場合や、オンデマンドモードで急激なトラフィック増加があった場合に、リクエストがスロットリングされます。これは、サービスの可用性に直結します(Errors)。

EC2/Fargate構成との違い:
RDSにはスロットリングという概念はありません。接続数の上限はありますが、DynamoDBのようにリクエスト単位でスロットリングされることはありません。

しきい値の考え方:

  • 警告・緊急とも: 1件でも発生したら即アラート

対策:

  • オンデマンドモードへの切り替え(トラフィックが予測不可能な場合)
  • プロビジョンドキャパシティの増加(プロビジョンドモードの場合)
  • Auto Scalingの設定見直し(プロビジョンドモードの場合)

CDKコード例:

const throttledRequestsAlarm = new cloudwatch.Alarm(this, 'DynamoDBThrottled', {
  metric: table.metricThrottledRequests({
    statistic: 'Sum',
    period: cdk.Duration.minutes(1),
  }),
  threshold: 1, // 1件でもアラート
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  alarmDescription: 'DynamoDBリクエストがスロットリングされています',
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

キャパシティモードの選択

DynamoDBには、オンデマンドモードプロビジョンドモードの2つのキャパシティモードがあります。

オンデマンドモード
  • 特徴: スロットリングの心配がほぼなし(急激な増加を除く)
  • 適用場面: トラフィックが予測不可能な場合、新しいアプリケーション
  • コスト: 読み書きリクエスト数に応じて変動(プロビジョンドより高額になる可能性)
  • 監視: ThrottledRequestsのみ監視(キャパシティ消費の監視は不要)
プロビジョンドモード
  • 特徴: キャパシティを事前に設定。Auto Scalingで自動調整可能
  • 適用場面: トラフィックが予測可能な場合、コストを最適化したい場合
  • コスト: 設定したキャパシティに対して時間課金(トラフィックが多い場合、オンデマンドより安価)
  • 監視: ConsumedReadCapacityUnitsConsumedWriteCapacityUnitsThrottledRequestsを監視

CDKコード例:

// オンデマンドモード
const tableonDemand = new dynamodb.Table(this, 'TableOnDemand', {
  partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, // オンデマンド
});

// プロビジョンドモード(Auto Scaling付き)
const tableProvisioned = new dynamodb.Table(this, 'TableProvisioned', {
  partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PROVISIONED,
  readCapacity: 5,
  writeCapacity: 5,
});

// Auto Scalingの設定
const readScaling = tableProvisioned.autoScaleReadCapacity({
  minCapacity: 5,
  maxCapacity: 100,
});

readScaling.scaleOnUtilization({
  targetUtilizationPercent: 70,
});

const writeScaling = tableProvisioned.autoScaleWriteCapacity({
  minCapacity: 5,
  maxCapacity: 100,
});

writeScaling.scaleOnUtilization({
  targetUtilizationPercent: 70,
});

CloudWatch Logs Insightsの活用

サーバレス構成では、CloudWatch Logs Insightsが特に重要です。Lambdaのログは自動的にCloudWatch Logsに送られ、Logs Insightsで詳細な分析が可能です。

Lambdaログの分析クエリ例

クエリ例1: Duration分布

Lambda関数の実行時間の分布を確認します。

fields @timestamp, @duration
| filter @type = "REPORT"
| stats avg(@duration) as avgDuration,
        max(@duration) as maxDuration,
        min(@duration) as minDuration,
        pct(@duration, 95) as p95Duration,
        pct(@duration, 99) as p99Duration

これにより、平均実行時間だけでなく、95パーセンタイル(p95)や99パーセンタイル(p99)も把握できます。p95が高い場合、一部のリクエストが非常に遅いことがわかります。

クエリ例2: エラー分析

Lambda関数で発生したエラーを時系列で確認します。

fields @timestamp, @message
| filter @message like /ERROR/ or @message like /Exception/
| sort @timestamp desc
| limit 100

エラーメッセージの内容を確認することで、問題の根本原因を特定できます。

クエリ例3: コールドスタート分析

コールドスタートの発生頻度と初期化時間を確認します。

fields @timestamp, @initDuration, @duration
| filter ispresent(@initDuration)
| stats avg(@initDuration) as avgInitDuration,
        max(@initDuration) as maxInitDuration,
        count() as coldStartCount

coldStartCountが多い場合、プロビジョンド同時実行数の設定を検討します。

クエリ例4: メモリ使用率

Lambda関数のメモリ使用状況を確認します。

fields @timestamp, @memorySize, @maxMemoryUsed
| filter @type = "REPORT"
| stats avg(@maxMemoryUsed / @memorySize * 100) as avgMemoryUtilization,
        max(@maxMemoryUsed) as maxMemoryUsed

メモリ使用率が低い場合、メモリサイズを減らしてコストを削減できます。逆に、使用率が高い場合、メモリ不足によるエラーのリスクがあります。

Logs Insightsのダッシュボード化

これらのクエリを、CloudWatch Dashboardのウィジェットとして追加することで、常時監視が可能になります。

X-Rayによる分散トレーシング

サーバレスアーキテクチャでは、複数のサービス(API Gateway、Lambda、DynamoDB)が連携して動作します。CloudWatch Logsだけでは、サービス間のレイテンシやボトルネックを特定することが困難です。

そこで、AWS X-Rayによる分散トレーシングが非常に有効です。X-Rayは、リクエストがサービス間をどのように流れているかを可視化し、パフォーマンスの問題を特定するのに役立ちます。

5.1 X-Rayとは

AWS X-Rayは、分散アプリケーションのトレーシングサービスです。リクエストが複数のサービスを通過する際の以下の情報を記録・可視化します:

  • サービスマップ: アプリケーションの構成要素(API Gateway、Lambda、DynamoDB)とその関係を可視化
  • トレース: 個々のリクエストがどのサービスをどの順序で通過したかを記録
  • セグメント: 各サービスでの処理時間とレイテンシ
  • アノテーション: カスタムメタデータの追加

5.2 なぜサーバレスでX-Rayが重要か

サーバレスアーキテクチャでは、以下の理由でX-Rayが特に有効です:

1. サービス間のレイテンシ分析

問題: CloudWatchメトリクスでは、Lambda全体のDurationは分かるが、「DynamoDBへのクエリが遅いのか」「Lambda内部のビジネスロジックが遅いのか」を区別できない。

X-Rayの解決策: トレースを見ることで、以下を明確に区別できます:

API Gateway (50ms)
  → Lambda Invocation (5ms)
    → Lambda Initialization (200ms) [コールドスタート]
    → Lambda Execution (300ms)
      → DynamoDB Query (250ms) [ボトルネック]
      → Business Logic (50ms)

2. コールドスタートの可視化

問題: CloudWatch Logsでコールドスタートは確認できるが、全体のレイテンシへの影響が見えにくい。

X-Rayの解決策: トレースでInitializationセグメントが表示され、コールドスタートがレイテンシ全体に与える影響を視覚的に理解できます。

3. エラーの根本原因特定

問題: Lambda関数でエラーが発生した場合、CloudWatch Logsだけでは「DynamoDBのエラーか」「Lambda内部のロジックエラーか」を特定しづらい。

X-Rayの解決策: エラーが発生したサービスをトレース上で赤く表示し、どこでエラーが発生したかを一目で確認できます。

4. サービスマップでボトルネック発見

問題: 複数のLambda関数やDynamoDBテーブルがある場合、どこがボトルネックかを特定するのが困難。

X-Rayの解決策: サービスマップで各サービスの平均レイテンシを可視化し、ボトルネックを一目で発見できます。

5.3 X-Rayで見られる情報

サービスマップ

アプリケーションの全体像を可視化します。

[Internet] → [API Gateway (平均50ms)] → [Lambda Function (平均300ms)] → [DynamoDB (平均100ms)]
                                            ↓
                                      [External API (平均500ms)] ← ボトルネック発見!

各サービスのノードには以下が表示されます:

  • 平均レイテンシ
  • リクエスト数
  • エラー率

トレース詳細

個々のリクエストの詳細な流れを確認できます。

トレース例:

リクエストID: 1-5f8e1234-abcd...
合計時間: 850ms

┌─ API Gateway: 850ms
│  └─ Lambda Function: 800ms
│     ├─ Initialization: 200ms (コールドスタート)
│     ├─ DynamoDB PutItem: 50ms
│     ├─ DynamoDB Query: 250ms ← 最も時間がかかっている
│     └─ Lambda Execution: 300ms

アノテーションとメタデータ

カスタムメタデータを追加することで、より詳細な分析が可能です:

import { captureAWS } from 'aws-xray-sdk-core';
import AWS from 'aws-sdk';

const dynamodb = captureAWS(new AWS.DynamoDB.DocumentClient());

export const handler = async (event: any) => {
  const segment = AWSXRay.getSegment();
  const subsegment = segment.addNewSubsegment('BusinessLogic');

  // アノテーション追加(検索可能)
  subsegment.addAnnotation('userId', event.userId);
  subsegment.addAnnotation('operation', 'createOrder');

  // メタデータ追加(詳細情報)
  subsegment.addMetadata('orderDetails', { items: event.items });

  try {
    // ビジネスロジック
    await dynamodb.put({ ... }).promise();
    subsegment.close();
  } catch (error) {
    subsegment.addError(error);
    subsegment.close();
    throw error;
  }
};

5.4 CDKでのX-Ray有効化

X-Rayは、AWS CDKで簡単に有効化できます。

Lambda関数でのX-Ray有効化

const lambdaFunction = new lambda.Function(this, 'Function', {
  runtime: lambda.Runtime.NODEJS_18_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda'),
  tracing: lambda.Tracing.ACTIVE, // X-Rayを有効化
});

これにより、Lambda関数が自動的にX-Rayにトレースを送信します。

API GatewayでのX-Ray有効化

const restApi = new apigateway.RestApi(this, 'Api', {
  restApiName: 'MyApi',
  deployOptions: {
    tracingEnabled: true, // X-Rayを有効化
  },
});

DynamoDBの自動トレース

DynamoDBへのリクエストは、AWS SDK v3でX-Rayが自動的にトレースされます。追加の設定は不要です。

ただし、Lambda関数内で以下のようにAWS SDKをX-Rayでラップする必要があります:

// Lambda関数コード (TypeScript)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { captureAWSv3Client } from 'aws-xray-sdk-core';

// DynamoDB ClientをX-Rayでラップ
const dynamodbClient = captureAWSv3Client(new DynamoDBClient({}));
const docClient = DynamoDBDocumentClient.from(dynamodbClient);

export const handler = async (event: any) => {
  // DynamoDBへのリクエストが自動的にトレースされる
  await docClient.send(new PutCommand({
    TableName: 'MyTable',
    Item: { id: '123', data: 'example' },
  }));

  return { statusCode: 200, body: 'OK' };
};

5.5 X-Rayを使ったパフォーマンス分析の実例

X-Rayを使うことで、以下のようなボトルネックを発見・解決できます。

DynamoDBクエリのボトルネック発見

問題: Lambda関数のDurationが平均800msと遅い。CloudWatchメトリクスではLambda全体のDurationしか分からず、どこがボトルネックか不明。

X-Rayでの分析:

トレース詳細:
- Lambda Execution: 800ms
  - DynamoDB Query: 700ms ← ボトルネック発見!
  - Business Logic: 100ms

X-Rayのトレースを見ることで、DynamoDBクエリが700ms(全体の87%)を占めていることが判明。

対策と結果:

  • GSI(グローバルセカンダリインデックス)を追加してクエリを最適化
  • 結果: DynamoDB Queryが100msに短縮、全体のDurationが300msに改善(62%削減)

このように、X-Rayはサービス間のレイテンシを詳細に分析でき、ボトルネックの特定と改善効果の測定に非常に有効です。コールドスタート分析、外部API呼び出しの問題特定なども同様に可視化できます。

5.6 X-Rayのベストプラクティス

効果的にX-Rayを活用するためのポイント:

  1. 本番環境でのみ有効化: X-Rayはトレース数に応じて課金(月100万トレースまで無料)。開発環境では無効化してコスト削減
  2. サンプリングレート調整: デフォルトは1秒あたり最初の1リクエスト + 5%のサンプリング。必要に応じて調整
  3. アノテーションの活用: subsegment.addAnnotation('userId', userId)でトレースに検索可能なメタデータを追加
  4. エラー記録: エラー発生時はsubsegment.addError(error)でX-Rayに記録し、トレース上で赤く表示

5.7 X-Ray vs CloudWatch Logs Insights

観点 CloudWatch Logs Insights AWS X-Ray
用途 ログ分析、Duration統計 サービス間のトレーシング
強み 詳細なログメッセージ検索 レイテンシのボトルネック特定
可視化 クエリ結果の表示 サービスマップ、トレースタイムライン
コスト ログ量に応じて課金 トレース数に応じて課金
使い分け エラーメッセージ分析、統計 パフォーマンス分析、ボトルネック発見

推奨: 両方を併用することで、包括的な監視とトラブルシューティングが可能になります。

CDKによる統合実装

ここまで説明した監視設計を、AWS CDKで実装する完全な例を紹介します。

プロジェクト構造

serverless-monitoring-stack/
├── lib/
│   ├── serverless-monitoring-stack.ts  # メインスタック
│   ├── alarms/
│   │   ├── apigateway-alarms.ts       # API Gatewayアラーム
│   │   ├── lambda-alarms.ts           # Lambdaアラーム
│   │   └── dynamodb-alarms.ts         # DynamoDBアラーム
│   └── dashboard/
│       └── monitoring-dashboard.ts    # 統合ダッシュボード
├── bin/
│   └── serverless-monitoring-stack.ts
├── package.json
└── tsconfig.json

メインスタック

// lib/serverless-monitoring-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import { Construct } from 'constructs';
import { ApiGatewayAlarms } from './alarms/apigateway-alarms';
import { LambdaAlarms } from './alarms/lambda-alarms';
import { DynamoDBAlarms } from './alarms/dynamodb-alarms';
import { MonitoringDashboard } from './dashboard/monitoring-dashboard';

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

    // SNS Topic for alarms
    const alarmTopic = new sns.Topic(this, 'AlarmTopic', {
      displayName: 'Serverless Monitoring Alarms',
    });

    alarmTopic.addSubscription(
      new subscriptions.EmailSubscription('your-email@example.com')
    );

    // DynamoDB Table
    const table = new dynamodb.Table(this, 'DataTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, // オンデマンドモード
      removalPolicy: cdk.RemovalPolicy.DESTROY, // 開発環境用
    });

    // Lambda Function
    const lambdaFunction = new lambda.Function(this, 'ApiFunction', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline(`
        const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
        const client = new DynamoDBClient({});

        exports.handler = async (event) => {
          const id = Date.now().toString();
          await client.send(new PutItemCommand({
            TableName: process.env.TABLE_NAME,
            Item: {
              id: { S: id },
              data: { S: JSON.stringify(event) },
            },
          }));

          return {
            statusCode: 200,
            body: JSON.stringify({ message: 'Success', id }),
          };
        };
      `),
      environment: {
        TABLE_NAME: table.tableName,
      },
      timeout: cdk.Duration.seconds(10),
      memorySize: 512,
    });

    table.grantWriteData(lambdaFunction);

    // API Gateway
    const api = new apigateway.RestApi(this, 'ServerlessApi', {
      restApiName: 'Serverless Monitoring Demo',
      deployOptions: {
        stageName: 'prod',
        metricsEnabled: true, // メトリクスを有効化
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
      },
    });

    const integration = new apigateway.LambdaIntegration(lambdaFunction);
    api.root.addMethod('POST', integration);

    // Alarms
    new ApiGatewayAlarms(this, 'ApiGatewayAlarms', {
      restApi: api,
      alarmTopic,
    });

    new LambdaAlarms(this, 'LambdaAlarms', {
      lambdaFunction,
      alarmTopic,
    });

    new DynamoDBAlarms(this, 'DynamoDBAlarms', {
      table,
      alarmTopic,
    });

    // Dashboard
    new MonitoringDashboard(this, 'MonitoringDashboard', {
      restApi: api,
      lambdaFunction,
      table,
    });

    // Outputs
    new cdk.CfnOutput(this, 'ApiUrl', {
      value: api.url,
      description: 'API Gateway URL',
    });

    new cdk.CfnOutput(this, 'TableName', {
      value: table.tableName,
      description: 'DynamoDB Table Name',
    });
  }
}

API Gatewayアラーム

// lib/alarms/apigateway-alarms.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

export interface ApiGatewayAlarmsProps {
  restApi: apigateway.RestApi;
  alarmTopic: sns.Topic;
}

export class ApiGatewayAlarms extends Construct {
  constructor(scope: Construct, id: string, props: ApiGatewayAlarmsProps) {
    super(scope, id);

    const { restApi, alarmTopic } = props;

    // 5XXエラーアラーム
    const serverErrorAlarm = new cloudwatch.Alarm(this, 'ServerErrorAlarm', {
      metric: restApi.metricServerError({
        statistic: 'Sum',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 5,
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'API Gateway 5XXエラーが発生しています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    serverErrorAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // 4XXエラーアラーム
    const clientErrorAlarm = new cloudwatch.Alarm(this, 'ClientErrorAlarm', {
      metric: restApi.metricClientError({
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 50,
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'API Gateway 4XXエラーが多発しています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    clientErrorAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // レイテンシアラーム
    const latencyAlarm = new cloudwatch.Alarm(this, 'LatencyAlarm', {
      metric: restApi.metricLatency({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 1000, // 1秒
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'API Gatewayのレイテンシが高くなっています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    latencyAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // IntegrationLatencyアラーム
    const integrationLatencyAlarm = new cloudwatch.Alarm(
      this,
      'IntegrationLatencyAlarm',
      {
        metric: restApi.metricIntegrationLatency({
          statistic: 'Average',
          period: cdk.Duration.minutes(5),
        }),
        threshold: 800,
        evaluationPeriods: 2,
        comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
        alarmDescription: 'Lambda統合レイテンシが高くなっています',
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }
    );

    integrationLatencyAlarm.addAlarmAction(
      new cloudwatch_actions.SnsAction(alarmTopic)
    );
  }
}

Lambdaアラーム

// lib/alarms/lambda-alarms.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

export interface LambdaAlarmsProps {
  lambdaFunction: lambda.Function;
  alarmTopic: sns.Topic;
}

export class LambdaAlarms extends Construct {
  constructor(scope: Construct, id: string, props: LambdaAlarmsProps) {
    super(scope, id);

    const { lambdaFunction, alarmTopic } = props;

    // Duration警告アラーム(タイムアウトの70%)
    const timeoutWarning = lambdaFunction.timeout.toMilliseconds() * 0.7;
    const durationWarningAlarm = new cloudwatch.Alarm(
      this,
      'DurationWarningAlarm',
      {
        metric: lambdaFunction.metricDuration({
          statistic: 'Average',
          period: cdk.Duration.minutes(5),
        }),
        threshold: timeoutWarning,
        evaluationPeriods: 2,
        comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
        alarmDescription: `Lambda実行時間がタイムアウトの70%を超えています`,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }
    );

    durationWarningAlarm.addAlarmAction(
      new cloudwatch_actions.SnsAction(alarmTopic)
    );

    // Duration緊急アラーム(タイムアウトの90%)
    const timeoutCritical = lambdaFunction.timeout.toMilliseconds() * 0.9;
    const durationCriticalAlarm = new cloudwatch.Alarm(
      this,
      'DurationCriticalAlarm',
      {
        metric: lambdaFunction.metricDuration({
          statistic: 'Average',
          period: cdk.Duration.minutes(5),
        }),
        threshold: timeoutCritical,
        evaluationPeriods: 1,
        comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
        alarmDescription: `Lambda実行時間がタイムアウトの90%を超えています`,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }
    );

    durationCriticalAlarm.addAlarmAction(
      new cloudwatch_actions.SnsAction(alarmTopic)
    );

    // エラーアラーム
    const errorAlarm = new cloudwatch.Alarm(this, 'ErrorAlarm', {
      metric: lambdaFunction.metricErrors({
        statistic: 'Sum',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 5,
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'Lambda関数でエラーが多発しています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    errorAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // エラー率アラーム
    const errorRateMetric = new cloudwatch.MathExpression({
      expression: 'm1 > 0 ? (m2 / m1) * 100 : 0',
      usingMetrics: {
        m1: lambdaFunction.metricInvocations({
          statistic: 'Sum',
          period: cdk.Duration.minutes(5),
        }),
        m2: lambdaFunction.metricErrors({
          statistic: 'Sum',
          period: cdk.Duration.minutes(5),
        }),
      },
      label: 'Error Rate (%)',
    });

    const errorRateAlarm = new cloudwatch.Alarm(this, 'ErrorRateAlarm', {
      metric: errorRateMetric,
      threshold: 5, // 5%
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'Lambdaエラー率が5%を超えています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    errorRateAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // スロットリングアラーム
    const throttleAlarm = new cloudwatch.Alarm(this, 'ThrottleAlarm', {
      metric: lambdaFunction.metricThrottles({
        statistic: 'Sum',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 1, // 1件でもアラート
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'Lambda関数がスロットリングされています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    throttleAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // 同時実行数アラーム
    const concurrentExecutionsAlarm = new cloudwatch.Alarm(
      this,
      'ConcurrentExecutionsAlarm',
      {
        metric: new cloudwatch.Metric({
          namespace: 'AWS/Lambda',
          metricName: 'ConcurrentExecutions',
          dimensionsMap: {
            FunctionName: lambdaFunction.functionName,
          },
          statistic: 'Maximum',
          period: cdk.Duration.minutes(1),
        }),
        threshold: 700, // アカウント制限1000の70%
        evaluationPeriods: 2,
        comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
        alarmDescription: 'Lambda同時実行数がアカウント制限の70%を超えています',
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }
    );

    concurrentExecutionsAlarm.addAlarmAction(
      new cloudwatch_actions.SnsAction(alarmTopic)
    );
  }
}

DynamoDBアラーム

// lib/alarms/dynamodb-alarms.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

export interface DynamoDBAlarmsProps {
  table: dynamodb.Table;
  alarmTopic: sns.Topic;
}

export class DynamoDBAlarms extends Construct {
  constructor(scope: Construct, id: string, props: DynamoDBAlarmsProps) {
    super(scope, id);

    const { table, alarmTopic } = props;

    // スロットリングアラーム
    const throttledRequestsAlarm = new cloudwatch.Alarm(
      this,
      'ThrottledRequestsAlarm',
      {
        metric: table.metricThrottledRequests({
          statistic: 'Sum',
          period: cdk.Duration.minutes(1),
        }),
        threshold: 1, // 1件でもアラート
        evaluationPeriods: 1,
        comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
        alarmDescription: 'DynamoDBリクエストがスロットリングされています',
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }
    );

    throttledRequestsAlarm.addAlarmAction(
      new cloudwatch_actions.SnsAction(alarmTopic)
    );

    // ユーザーエラーアラーム
    const userErrorsAlarm = new cloudwatch.Alarm(this, 'UserErrorsAlarm', {
      metric: table.metricUserErrors({
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 10,
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'DynamoDBユーザーエラーが発生しています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    userErrorsAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(alarmTopic));

    // システムエラーアラーム
    const systemErrorsAlarm = new cloudwatch.Alarm(this, 'SystemErrorsAlarm', {
      metric: table.metricSystemErrorsForOperations({
        operations: [dynamodb.Operation.PUT_ITEM, dynamodb.Operation.GET_ITEM],
        statistic: 'Sum',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 1, // 1件でもアラート
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      alarmDescription: 'DynamoDBシステムエラーが発生しています',
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    systemErrorsAlarm.addAlarmAction(
      new cloudwatch_actions.SnsAction(alarmTopic)
    );
  }
}

統合ダッシュボード

CloudWatch Dashboardで、以下のメトリクスを可視化します。

// lib/dashboard/monitoring-dashboard.ts
const dashboard = new cloudwatch.Dashboard(this, 'Dashboard', {
  dashboardName: 'ServerlessMonitoringDashboard',
});

// API Gateway メトリクス
dashboard.addWidgets(
  new cloudwatch.GraphWidget({
    title: 'API Gateway - Latency',
    left: [
      restApi.metricLatency({ statistic: 'Average' }),
      restApi.metricIntegrationLatency({ statistic: 'Average' }),
    ],
  }),
  new cloudwatch.GraphWidget({
    title: 'API Gateway - Errors',
    left: [
      restApi.metricClientError({ statistic: 'Sum' }),
      restApi.metricServerError({ statistic: 'Sum' }),
    ],
  })
);

// Lambda メトリクス
dashboard.addWidgets(
  new cloudwatch.GraphWidget({
    title: 'Lambda - Duration',
    left: [
      lambdaFunction.metricDuration({ statistic: 'Average' }),
      lambdaFunction.metricDuration({ statistic: 'Maximum' }),
    ],
  }),
  new cloudwatch.GraphWidget({
    title: 'Lambda - Invocations & Errors',
    left: [lambdaFunction.metricInvocations()],
    right: [lambdaFunction.metricErrors()],
  })
);

// DynamoDB メトリクス
dashboard.addWidgets(
  new cloudwatch.GraphWidget({
    title: 'DynamoDB - Throttled Requests & Errors',
    left: [
      table.metricThrottledRequests({ statistic: 'Sum' }),
      table.metricUserErrors({ statistic: 'Sum' }),
    ],
  })
);

同様の方法で、Lambda Throttles、ConcurrentExecutions、API Gateway Request Countなどのウィジェットを追加することで、サーバレスアーキテクチャ全体を一つのダッシュボードで監視できます。

EC2/Fargate構成からの移行ポイント

EC2やFargate構成からサーバレス構成に移行する際、監視設計も大きく変わります。

監視設計の変更点

項目 EC2/Fargate構成 サーバレス構成 移行アクション
監視単位 インスタンス/タスク 関数実行 実行時間・エラー・Throttleを監視
リソース監視 CPU/メモリ使用率 Duration(実行時間) メトリクスをDurationに変更
スケーリング監視 インスタンス/タスク数 同時実行数 ConcurrentExecutionsを監視
スロットリング なし(通常) Throttles(重要) 1件でもアラート設定
ログ収集 Agent/タスク定義 自動(CloudWatch Logs) 設定不要、Logs Insights活用
データベース監視 RDS(接続数、CPU) DynamoDB(キャパシティ) キャパシティ消費を監視
エントリーポイント ALB API Gateway IntegrationLatencyも監視
コスト監視 インスタンス時間 実行時間 × メモリ Durationがコストに直結

移行時の具体的な手順

ステップ1: メトリクスの対応関係を理解

従来型/コンテナ構成とサーバレス構成では、監視するメトリクスが異なります。以下の対応関係を理解しましょう。

  • CPU使用率Duration(実行時間)
  • メモリ使用率 → Lambda関数のメモリ設定(CloudWatch Logsで実際の使用量を確認)
  • インスタンス数/タスク数ConcurrentExecutions(同時実行数)
  • RDS接続数 → DynamoDBには対応するメトリクスなし(キャパシティ消費を監視)

ステップ2: アラームの再設計

EC2/Fargateで設定していたアラームを、サーバレス用に再設計します。

  1. Duration監視: タイムアウトの70%で警告、90%で緊急
  2. Errors監視: 1分間に5件以上でアラート
  3. Throttles監視: 1件でもアラート(最重要)
  4. DynamoDB ThrottledRequests監視: 1件でもアラート

ステップ3: CloudWatch Logs Insightsの活用

Lambdaのログは自動的にCloudWatch Logsに送られます。Logs Insightsで以下を分析しましょう。

  • コールドスタートの発生頻度と初期化時間
  • Duration分布(p95、p99)
  • エラーメッセージの内容
  • メモリ使用状況

ステップ4: ダッシュボードの作成

CloudWatch Dashboardで、以下のメトリクスを可視化します。

  • API Gateway: Latency、IntegrationLatency、Errors、Count
  • Lambda: Duration、Errors、Throttles、ConcurrentExecutions
  • DynamoDB: ThrottledRequests、UserErrors、SystemErrors

移行時の注意点

1. コールドスタート対策

課題: Lambda関数は、初回実行時や長時間未使用後の実行時に、実行環境の初期化(コールドスタート)が発生します。これにより、Durationが通常の数倍から数十倍に増加することがあります。

対策:

  • プロビジョンド同時実行数: レイテンシが重要なエンドポイントには、プロビジョンド同時実行数を設定(コスト増加に注意)
  • Lambda SnapStart: Javaランタイムのみ対応。起動時間を最大10倍短縮
  • メモリサイズ増加: CPUも比例して増加するため、初期化が速くなる
  • 軽量な依存関係: 不要なライブラリを削除し、関数のパッケージサイズを削減

検証方法:
CloudWatch Logs Insightsでコールドスタートを分析し、ビジネス要件に照らして対策が必要か判断します。

fields @timestamp, @initDuration, @duration
| filter ispresent(@initDuration)
| stats avg(@initDuration) as avgInitDuration,
        max(@initDuration) as maxInitDuration,
        count() as coldStartCount

2. 同時実行数制限

課題: Lambdaには、アカウントレベルでデフォルト1000という同時実行数の制限があります。この制限に達すると、Throttlesが発生します。

対策:

  • 予約済み同時実行数の設定: 重要な関数に優先的にリソースを割り当て
  • アカウント制限の引き上げ申請: AWSサポートに申請(通常は数千〜数万まで引き上げ可能)
  • Duration短縮: 関数の実行時間が短いほど、同時実行数は減少
  • 非同期処理: SQSやEventBridgeを使用して、処理を非同期化

計算例:

  • リクエスト: 1000 req/sec
  • Duration: 100ms
  • 必要な同時実行数: 1000 × 0.1 = 100

Durationが1000ms(1秒)の場合、同時実行数は1000になり、制限に達します。

3. タイムアウト設定

課題: Lambda関数の最大実行時間は15分です。長時間処理が必要な場合は、設計変更が必要です。

対策:

  • 適切なタイムアウト値: デフォルト3秒は短すぎることが多い。実際の処理時間に応じて設定(例: 10秒、30秒)
  • Step Functionsへの分割: 15分を超える処理は、Step Functionsで複数のLambda関数に分割
  • 非同期処理: 長時間処理は、SQSやStep Functionsで非同期化

4. DynamoDBのキャパシティモード選択

課題: DynamoDBのキャパシティモード(オンデマンド vs プロビジョンド)の選択が難しい。

対策:

  • オンデマンドモード: トラフィックが予測不可能な場合、新しいアプリケーション、プロトタイプ
  • プロビジョンドモード: トラフィックが予測可能で、コストを最適化したい場合

移行時は、まずオンデマンドモードでスタートし、トラフィックパターンが安定してからプロビジョンドモードへの切り替えを検討することをお勧めします。

コスト最適化

サーバレスでは、コスト最適化が特に重要です。実行時間課金のため、監視設計とコスト管理が密接に関係します。

Lambdaのコスト最適化

1. メモリサイズの最適化

Lambdaは「実行時間 × メモリサイズ」で課金されます。メモリサイズを増やすとCPU性能も上がるため、「高メモリ × 短時間」と「低メモリ × 長時間」のトレードオフがあります。

最適化手順:

  1. AWS Lambda Power Tuningツールを使用
  2. 様々なメモリサイズで関数を実行し、コスト最小値を見つける
  3. CloudWatch Logsで実際のメモリ使用量を確認し、過剰なメモリを削減

:

  • 512MBで実行時間1000ms: 約$0.00001667
  • 1024MBで実行時間600ms: 約$0.00001000(コスト削減)

メモリサイズを2倍にしても、実行時間が40%短縮されれば、トータルコストは削減されます。

2. Duration(実行時間)の短縮

Durationは、コストに直結する最も重要な指標です。

最適化手順:

  • 不要な処理の削除
  • 並列処理の活用(Promise.allなど)
  • 外部APIコールの最適化(タイムアウト設定、キャッシング)
  • DynamoDB BatchGetItemの活用(複数アイテムを一度に取得)
  • Lambda Layersでの共通ライブラリ管理

3. プロビジョンド同時実行数の使い分け

プロビジョンド同時実行数は、コールドスタートを防ぐ効果的な手段ですが、コストが大幅に増加します(常時課金)。

使い分け:

  • 使用すべき: レイテンシが極めて重要なエンドポイント(例: 決済API、リアルタイム通知)
  • 使用しない: バッチ処理、管理画面、非同期処理

コスト例:

  • 通常実行: 100万リクエスト、Duration 100ms、512MB → 約$0.83
  • プロビジョンド: 10同時実行、24時間 → 約$40/月(約48倍)

DynamoDBのコスト最適化

1. キャパシティモードの選択

オンデマンドモード:

  • コスト: 読み取り約$0.25/100万リクエスト、書き込み約$1.25/100万リクエスト
  • 適用: トラフィック予測不可能、月間読み取り<約7億リクエスト

プロビジョンドモード(予約キャパシティ付き):

  • コスト: 1RCU約$0.00013/時間、1WCU約$0.00065/時間(予約により更に削減)
  • 適用: トラフィック予測可能、大量リクエスト

2. GSI(グローバルセカンダリインデックス)の最小化

GSIごとに追加のキャパシティコストが発生します。必要最小限に抑えましょう。

3. TTL(Time To Live)の活用

不要なデータを自動削除することで、ストレージコストを削減できます。

const table = new dynamodb.Table(this, 'TableWithTTL', {
  partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
  timeToLiveAttribute: 'ttl', // TTL属性
});

まとめ

サーバレスアーキテクチャの監視設計について、EC2/Fargate構成との違いを中心に解説しました。

EC2/Fargate構成との主な違い

  1. 監視単位: インスタンス/タスク → 関数実行
  2. 主要メトリクス: CPU/メモリ → Duration/Errors/Throttles
  3. スケーリング: 手動/自動設定 → 完全自動(同時実行数制限のみ管理)
  4. ログ収集: Agent/設定必要 → 自動(CloudWatch Logs Insights活用)
  5. データベース: RDS(接続数・クエリ監視) → DynamoDB(キャパシティ監視)

サーバレス監視の重要ポイント

  1. コールドスタート: CloudWatch Logs InsightsとX-RayでInit Durationを分析。必要に応じてプロビジョンド同時実行数を設定
  2. Throttling: Lambda/DynamoDB両方で監視。1件でも発生したら即アラート・対策
  3. 同時実行数: アカウント制限の70%でアラート。Throttles発生前に対策
  4. Duration: タイムアウトとコストに直結。70%で警告、90%で緊急アラート
  5. キャパシティ: DynamoDBのモード選択とスロットリング監視が重要
  6. X-Rayによる分散トレーシング: サービス間のレイテンシとボトルネックを可視化。パフォーマンス分析に非常に有効

次のステップ

サーバレス監視を実装した後は、以下のステップでさらに改善できます。

  1. Lambda Power Tuning: メモリサイズを最適化し、コストとパフォーマンスのバランスを最適化
  2. CloudWatch Logs Insights: ログ分析を定期的に実施し、パフォーマンス改善の機会を発見
  3. X-Rayのアノテーション活用: カスタムメタデータを追加し、より詳細な分析を実施
  4. X-Rayサンプリングルールの最適化: トレース数とコストのバランスを調整
  5. CloudWatch Contributor Insights: 最も影響の大きいリクエストを特定
  6. コスト分析: CloudWatch Cost ExplorerでLambda・DynamoDBのコストを定期的に確認

サーバレスアーキテクチャは、適切な監視設計により、高可用性かつコスト効率の良いシステムを実現できます。本記事が、皆さんのサーバレス監視設計の一助となれば幸いです。

13
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?