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?

何を監視すべきか迷わないために - AWS CloudWatchで実装するWebアプリ監視設計パターン

13
Last updated at Posted at 2026-02-20

はじめに

Webアプリケーションを本番環境で運用する際、監視は不可欠です。しかし、いざ監視を設定しようとすると、こんな悩みに直面することがあります:

  • 「CloudWatchのメトリクスが多すぎて、何を監視すればいいか分からない」
  • 「すべて監視するとコストが心配」
  • 「重要なメトリクスを見逃して、障害に気づけなかったら...」

本記事では、シンプルな3層構成(ALB + EC2 + RDS)のWebアプリケーションを例に、「何を監視すべきか」の判断基準と、AWS公式の推奨アラームを活用した実践的な監視設計を紹介します。

まずは結論

  1. すべてを監視する必要はない

    • Four Golden Signals(Latency、Traffic、Errors、Saturation)を基準に
    • ビジネスへの影響度で優先順位をつける
  2. AWS公式の推奨アラームを活用

    • CloudWatchが提供する推奨アラームは、AWSの運用ノウハウの結晶
    • しきい値も事前設定されており、すぐに使える
  3. 各レイヤーで監視すべきメトリクス

    • ALB: TargetResponseTime、HTTPCode_Target_5XX_Count、HealthyHostCount
    • EC2: CPUUtilization、StatusCheckFailed、メモリ・ディスク使用率
    • RDS: DatabaseConnections、CPUUtilization、FreeableMemory、レイテンシ

直面した課題

構成:シンプルな3層アーキテクチャ

私たちが運用していたWebアプリケーションは、以下の構成でした:

Internet
    ↓
[ALB] ← ロードバランサー層
    ↓
[EC2] ← アプリケーション層
    ↓
[RDS] ← データベース層
  • ALB(Application Load Balancer): HTTPSリクエストを受け付け、EC2インスタンスに振り分け
  • EC2: Webアプリケーション(Node.js/Python/Java等)が稼働
  • RDS(PostgreSQL/MySQL): アプリケーションデータを永続化

この構成は多くのWebアプリケーションで採用されており、シンプルで理解しやすいアーキテクチャです。

監視を始めようとしたときの悩み

本番リリースを控え、監視を設定しようとしたときに直面した問題:

  1. メトリクスが多すぎる

    • ALBだけで20種類以上のメトリクス
    • EC2、RDSも同様に多数のメトリクスが存在
    • 「どれが重要なのか」の判断基準がない
  2. コストへの懸念

    • CloudWatchアラームは1つあたり月額約$0.10
    • カスタムメトリクスはさらに高額
    • すべてを監視すると月数十ドル以上のコスト増
  3. 見逃しへの不安

    • 重要なメトリクスを監視し忘れたら?
    • 障害が発生してから気づくのは遅すぎる

この状況で、「何を監視すべきか」の明確な基準が必要でした。

監視設計の2つのアプローチ

「何を監視すべきか」を判断するため、2つのアプローチを組み合わせます。

3.1 理論的フレームワーク:Four Golden Signals

Google SREが提唱するFour Golden Signalsは、監視設計の業界標準フレームワークです。

1. Latency(レイテンシ)

定義: リクエストの応答時間
重要性: ユーザー体験に直結。遅いレスポンスはユーザー離脱につながる
: APIの応答時間、データベースクエリ時間

2. Traffic(トラフィック)

定義: システムへの需要(リクエスト数、スループット)
重要性: 負荷の増減を把握し、スケーリングの判断材料にする
: 秒間リクエスト数、データベース接続数

3. Errors(エラー)

定義: 失敗したリクエストの割合
重要性: サービスの可用性を示す最も重要な指標
: HTTP 5xxエラー率、データベース接続エラー

4. Saturation(飽和度)

定義: リソースの使用率(キャパシティの限界に近いか)
重要性: リソース枯渇を事前に検知し、スケールアウトやチューニングの機会を得る
: CPU使用率、メモリ使用率、ディスク容量

このフレームワークにより、「なぜこのメトリクスを監視するのか」が明確になります。

3.2 AWS公式の実践ガイド:CloudWatch推奨アラーム

AWSは、各サービスごとに推奨アラームを提供しています。これは、AWSが長年の運用経験から「このサービスではこのメトリクスを監視すべき」と推奨するものです。

CloudWatch推奨アラームの特徴:

  • ✅ AWS公式のベストプラクティスに基づく
  • ✅ しきい値が事前設定されている(調整も可能)
  • ✅ CloudFormation/CDK形式でエクスポート可能
  • ✅ 追加料金なし(アラーム作成後は通常料金)

活用方法:

  1. CloudWatchコンソール → Metrics → All metrics
  2. 「アラームの推奨事項」フィルターを選択
  3. サービス名前空間(ALB、EC2、RDSなど)を選択
  4. 推奨アラームを確認

本記事では、この推奨アラームをベースに、Four Golden Signalsの観点から各メトリクスを解説します。

各レイヤーの監視設計

4.1 ALB(ロードバランサー層)

ALBは、インターネットからのリクエストを最初に受け付けるレイヤーです。ここでの異常は、即座にユーザー体験に影響します。

必須メトリクス

1. TargetResponseTime(ターゲット応答時間)

何を測定するか:
ALBからバックエンド(EC2)へリクエストを送信してから、レスポンスを受け取るまでの時間。

なぜ重要か:

  • ユーザー体験に直結する指標(Latency
  • アプリケーションのパフォーマンス劣化を検知
  • データベーススロークエリやアプリケーションバグの兆候

しきい値の考え方:

  • 目標レスポンス時間: 通常200ms以下が理想
  • 警告: 500ms以上(ユーザーが遅いと感じ始める)
  • 緊急: 1000ms以上(ユーザー離脱のリスク)

CloudWatch推奨値: 通常500ms〜1000msが推奨される

// CDKでの実装例
new cloudwatch.Alarm(this, 'ALBHighLatency', {
  metric: targetGroup.metricTargetResponseTime(),
  threshold: 0.5, // 500ms
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
2. HTTPCode_Target_5XX_Count(5xxエラー数)

何を測定するか:
バックエンド(EC2)から返された5xxエラー(サーバーエラー)の数。

なぜ重要か:

  • サービスの可用性を示す(Errors
  • アプリケーションのバグやインフラ障害を検知
  • ユーザーにエラー画面が表示されている状態

しきい値の考え方:

  • 理想: 0件(5xxエラーは本来発生すべきでない)
  • 警告: 1分間に5件以上
  • 緊急: 1分間に20件以上または全体の1%以上

CloudWatch推奨値: 通常5〜10件/分が推奨される

new cloudwatch.Alarm(this, 'ALBServerErrors', {
  metric: targetGroup.metricHttpCodeTarget(
    elbv2.HttpCodeTarget.TARGET_5XX_COUNT
  ),
  threshold: 5,
  evaluationPeriods: 1,
  datapointsToAlarm: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  statistic: 'Sum',
  period: cdk.Duration.minutes(1),
});
3. HealthyHostCount(正常なホスト数)

何を測定するか:
ALBのヘルスチェックに成功しているEC2インスタンスの数。

なぜ重要か:

  • システムの可用性を示す(Saturation + Errors
  • すべてのインスタンスがダウンすると、サービス全停止
  • スケーリング不足の検知

しきい値の考え方:

  • 最小: 2台以上(冗長性の確保)
  • 警告: 想定台数の50%以下
  • 緊急: 1台以下(単一障害点)

CloudWatch推奨値: 通常1台以下でアラート

new cloudwatch.Alarm(this, 'ALBUnhealthyHosts', {
  metric: targetGroup.metricHealthyHostCount(),
  threshold: 1,
  evaluationPeriods: 1,
  datapointsToAlarm: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
});
4. RequestCount(リクエスト数)

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

なぜ重要か:

  • トラフィックの傾向を把握(Traffic
  • 急激な増減は異常の兆候
    • 急増: DDoS攻撃、バイラル拡散
    • 急減: サービス障害、DNS問題

しきい値の考え方:

  • 通常トラフィックのベースラインを把握
  • 平常時の2倍以上: 注意
  • 平常時の5倍以上: 緊急(スケーリング必要)

CloudWatch推奨値: ベースラインの150%〜200%

// トラフィック監視はダッシュボードで傾向を見るのが一般的
// 異常検知アラームとしてはAnomalyDetectorを使用
new cloudwatch.Alarm(this, 'ALBTrafficAnomaly', {
  metric: targetGroup.metricRequestCount(),
  threshold: 1000, // 自環境に合わせて調整
  evaluationPeriods: 3,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});

推奨メトリクス(オプション)

HTTPCode_Target_4XX_Count(4xxエラー数)

何を測定するか:
バックエンドから返された4xxエラー(クライアントエラー)の数。

なぜ重要か:

  • クライアント側の問題を検知(Errors
  • 404エラー急増: リンク切れ、URL変更
  • 401/403エラー急増: 認証問題

監視すべきか:

  • 必須ではない(クライアント側の問題のため)
  • ただし、急増は異常の兆候なので傾向監視は推奨

4.2 EC2(アプリケーション層)

EC2は、ビジネスロジックが実行されるレイヤーです。リソース枯渇やアプリケーションクラッシュを早期に検知する必要があります。

必須メトリクス

1. CPUUtilization(CPU使用率)

何を測定するか:
EC2インスタンスのCPU使用率(0〜100%)。

なぜ重要か:

  • リソースの飽和度を示す(Saturation
  • 高CPU使用率が継続すると、レスポンス遅延やタイムアウト
  • スケールアウトの判断材料

しきい値の考え方:

  • 理想: 50%以下(余裕がある状態)
  • 警告: 70%以上が5分以上継続
  • 緊急: 90%以上が継続(即座にスケールアウト必要)

CloudWatch推奨値: 通常80%以上

new cloudwatch.Alarm(this, 'EC2HighCPU', {
  metric: new cloudwatch.Metric({
    namespace: 'AWS/EC2',
    metricName: 'CPUUtilization',
    dimensionsMap: {
      InstanceId: instance.instanceId,
    },
    statistic: 'Average',
  }),
  threshold: 80,
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
2. StatusCheckFailed(ステータスチェック失敗)

何を測定するか:
EC2インスタンスのシステムステータスチェックとインスタンスステータスチェックの失敗。

なぜ重要か:

  • インスタンスの可用性を示す(Errors
  • ハードウェア障害、ネットワーク障害、OS障害を検知
  • 失敗時は自動的にインスタンス再起動などのリカバリーが必要

しきい値の考え方:

  • 失敗 = 1(0 = 正常、1 = 失敗)
  • 1度でも失敗したら即座にアラート

CloudWatch推奨値: 1(失敗)

new cloudwatch.Alarm(this, 'EC2StatusCheckFailed', {
  metric: new cloudwatch.Metric({
    namespace: 'AWS/EC2',
    metricName: 'StatusCheckFailed',
    dimensionsMap: {
      InstanceId: instance.instanceId,
    },
    statistic: 'Maximum',
  }),
  threshold: 1,
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
});

推奨メトリクス(CloudWatch Agent必要)

EC2のデフォルトメトリクスには、メモリとディスクの情報が含まれません。これらを監視するには、CloudWatch Agentのインストールが必要です。

3. メモリ使用率(mem_used_percent)

何を測定するか:
EC2インスタンスのメモリ(RAM)使用率。

なぜ重要か:

  • メモリリークやメモリ不足を検知(Saturation
  • メモリ枯渇時、OOM KillerがプロセスをKillする
  • スワップが発生するとパフォーマンスが激減

しきい値の考え方:

  • 警告: 80%以上
  • 緊急: 95%以上(OOMリスク)
new cloudwatch.Alarm(this, 'EC2HighMemory', {
  metric: new cloudwatch.Metric({
    namespace: 'CWAgent',
    metricName: 'mem_used_percent',
    dimensionsMap: {
      InstanceId: instance.instanceId,
    },
    statistic: 'Average',
  }),
  threshold: 80,
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
4. ディスク使用率(disk_used_percent)

何を測定するか:
EC2インスタンスのディスク使用率。

なぜ重要か:

  • ディスク容量不足を検知(Saturation
  • ログファイルの肥大化、一時ファイルの蓄積
  • ディスク満杯時、アプリケーションが書き込めずにクラッシュ

しきい値の考え方:

  • 警告: 80%以上(クリーンアップ推奨)
  • 緊急: 90%以上(即座に対応必要)
new cloudwatch.Alarm(this, 'EC2HighDisk', {
  metric: new cloudwatch.Metric({
    namespace: 'CWAgent',
    metricName: 'disk_used_percent',
    dimensionsMap: {
      InstanceId: instance.instanceId,
      path: '/',
      fstype: 'ext4',
    },
    statistic: 'Average',
  }),
  threshold: 80,
  evaluationPeriods: 1,
  datapointsToAlarm: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});

4.3 RDS(データベース層)

RDSは、アプリケーションデータを保持する最も重要なレイヤーです。データベースの障害は、サービス全体の停止につながります。

必須メトリクス

1. DatabaseConnections(データベース接続数)

何を測定するか:
RDSインスタンスへのアクティブな接続数。

なぜ重要か:

  • データベースの飽和度を示す(Saturation
  • 接続数上限に達すると、新しい接続が拒否される
  • アプリケーションのコネクションプールのリーク検知

しきい値の考え方:

  • RDSインスタンスの最大接続数を確認(例: db.t3.medium = 約400接続)
  • 警告: 最大接続数の80%
  • 緊急: 最大接続数の95%

CloudWatch推奨値: 最大接続数の80%

new cloudwatch.Alarm(this, 'RDSHighConnections', {
  metric: database.metricDatabaseConnections(),
  threshold: 320, // db.t3.mediumの場合(400の80%)
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
2. CPUUtilization(CPU使用率)

何を測定するか:
RDSインスタンスのCPU使用率。

なぜ重要か:

  • データベースの飽和度を示す(Saturation
  • 高CPU使用率は、スロークエリやインデックス不足の兆候
  • 継続すると、クエリタイムアウトやレスポンス遅延

しきい値の考え方:

  • 警告: 70%以上が5分以上継続
  • 緊急: 90%以上(クエリ最適化またはスケールアップ必要)

CloudWatch推奨値: 80%以上

new cloudwatch.Alarm(this, 'RDSHighCPU', {
  metric: database.metricCPUUtilization(),
  threshold: 80,
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
3. FreeableMemory(空きメモリ)

何を測定するか:
RDSインスタンスの空きメモリ容量(バイト単位)。

なぜ重要か:

  • メモリ不足を検知(Saturation
  • データベースはメモリをキャッシュに使用
  • メモリ不足時、ディスクI/Oが増加してパフォーマンス劣化

しきい値の考え方:

  • 警告: 空きメモリが総メモリの10%以下
  • 緊急: 空きメモリが総メモリの5%以下

CloudWatch推奨値: 100MB以下(インスタンスサイズによる)

new cloudwatch.Alarm(this, 'RDSLowMemory', {
  metric: database.metricFreeableMemory(),
  threshold: 100 * 1024 * 1024, // 100MB(バイト単位)
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
});
4. ReadLatency / WriteLatency(読み取り/書き込みレイテンシ)

何を測定するか:
ディスクI/O操作の平均レイテンシ(ミリ秒単位)。

なぜ重要か:

  • データベースのパフォーマンスを示す(Latency
  • 高レイテンシは、スロークエリやディスクI/O負荷の兆候
  • アプリケーションのレスポンス遅延に直結

しきい値の考え方:

  • 理想: 5ms以下
  • 警告: 10ms以上
  • 緊急: 20ms以上(IOPS不足、スケールアップ検討)

CloudWatch推奨値: 10ms以上

new cloudwatch.Alarm(this, 'RDSHighReadLatency', {
  metric: database.metricReadLatency(),
  threshold: 0.01, // 10ms(秒単位)
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});

new cloudwatch.Alarm(this, 'RDSHighWriteLatency', {
  metric: database.metricWriteLatency(),
  threshold: 0.01, // 10ms(秒単位)
  evaluationPeriods: 2,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});

推奨メトリクス(オプション)

5. FreeStorageSpace(空きストレージ容量)

何を測定するか:
RDSインスタンスの空きディスク容量。

なぜ重要か:

  • ディスク容量不足を検知(Saturation
  • ディスク満杯時、データベースが書き込みできなくなる
  • ログファイルやバックアップの蓄積

しきい値の考え方:

  • 警告: 空き容量が総容量の10%以下
  • 緊急: 空き容量が5GB以下
new cloudwatch.Alarm(this, 'RDSLowStorage', {
  metric: database.metricFreeStorageSpace(),
  threshold: 5 * 1024 * 1024 * 1024, // 5GB(バイト単位)
  evaluationPeriods: 1,
  datapointsToAlarm: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
});

CDKによる統合実装

ここまでの監視設計をTypeScript CDKで実装します。

5.1 プロジェクト構造

monitoring-stack/
├── bin/
│   └── app.ts
├── lib/
│   ├── monitoring-stack.ts          # メインスタック
│   ├── alarms/
│   │   ├── alb-alarms.ts            # ALBアラーム
│   │   ├── ec2-alarms.ts            # EC2アラーム
│   │   └── rds-alarms.ts            # RDSアラーム
│   └── dashboard/
│       └── monitoring-dashboard.ts  # 統合ダッシュボード
├── package.json
└── tsconfig.json

5.2 メインスタック

// lib/monitoring-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import { Construct } from 'constructs';
import { AlbAlarms } from './alarms/alb-alarms';
import { Ec2Alarms } from './alarms/ec2-alarms';
import { RdsAlarms } from './alarms/rds-alarms';
import { MonitoringDashboard } from './dashboard/monitoring-dashboard';

interface MonitoringStackProps extends cdk.StackProps {
  albArn: string;
  targetGroupArn: string;
  instanceId: string;
  dbInstanceIdentifier: string;
  alertEmail: string;
}

export class MonitoringStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: MonitoringStackProps) {
    super(scope, id, props);

    // SNSトピックの作成(アラート通知先)
    const alarmTopic = new sns.Topic(this, 'AlarmTopic', {
      displayName: 'Web Application Monitoring Alarms',
      topicName: 'web-app-monitoring-alarms',
    });

    // メール通知の設定
    alarmTopic.addSubscription(
      new subscriptions.EmailSubscription(props.alertEmail)
    );

    // ALBアラームの作成
    new AlbAlarms(this, 'AlbAlarms', {
      albArn: props.albArn,
      targetGroupArn: props.targetGroupArn,
      alarmTopic,
    });

    // EC2アラームの作成
    new Ec2Alarms(this, 'Ec2Alarms', {
      instanceId: props.instanceId,
      alarmTopic,
    });

    // RDSアラームの作成
    new RdsAlarms(this, 'RdsAlarms', {
      dbInstanceIdentifier: props.dbInstanceIdentifier,
      alarmTopic,
    });

    // 統合ダッシュボードの作成
    new MonitoringDashboard(this, 'Dashboard', {
      albArn: props.albArn,
      instanceId: props.instanceId,
      dbInstanceIdentifier: props.dbInstanceIdentifier,
    });

    // 出力
    new cdk.CfnOutput(this, 'AlarmTopicArn', {
      value: alarmTopic.topicArn,
      description: 'SNS Topic ARN for alarms',
    });
  }
}

5.3 ALBアラーム

// lib/alarms/alb-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 elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

interface AlbAlarmsProps {
  albArn: string;
  targetGroupArn: string;
  alarmTopic: sns.ITopic;
}

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

    // ALBの参照取得
    const alb = elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(
      this,
      'ALB',
      {
        loadBalancerArn: props.albArn,
        securityGroupId: '', // ダミー値
      }
    );

    // ターゲットグループの参照取得
    const targetGroup = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(
      this,
      'TargetGroup',
      {
        targetGroupArn: props.targetGroupArn,
      }
    );

    // 1. TargetResponseTime(レイテンシ)
    const highLatencyAlarm = new cloudwatch.Alarm(this, 'ALBHighLatency', {
      alarmName: 'ALB-HighLatency',
      alarmDescription: 'ALB target response time is high',
      metric: targetGroup.metricTargetResponseTime({
        statistic: 'Average',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 0.5, // 500ms
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });
    highLatencyAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 2. HTTPCode_Target_5XX_Count(5xxエラー)
    const serverErrorsAlarm = new cloudwatch.Alarm(this, 'ALBServerErrors', {
      alarmName: 'ALB-ServerErrors',
      alarmDescription: 'ALB is returning 5xx errors',
      metric: targetGroup.metricHttpCodeTarget(
        elbv2.HttpCodeTarget.TARGET_5XX_COUNT,
        {
          statistic: 'Sum',
          period: cdk.Duration.minutes(1),
        }
      ),
      threshold: 5,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });
    serverErrorsAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 3. HealthyHostCount(正常なホスト数)
    const unhealthyHostsAlarm = new cloudwatch.Alarm(this, 'ALBUnhealthyHosts', {
      alarmName: 'ALB-UnhealthyHosts',
      alarmDescription: 'ALB has unhealthy hosts',
      metric: targetGroup.metricHealthyHostCount({
        statistic: 'Minimum',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 1,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.BREACHING,
    });
    unhealthyHostsAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));
  }
}

5.4 EC2アラーム

// lib/alarms/ec2-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 sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

interface Ec2AlarmsProps {
  instanceId: string;
  alarmTopic: sns.ITopic;
}

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

    // 1. CPUUtilization(CPU使用率)
    const highCpuAlarm = new cloudwatch.Alarm(this, 'EC2HighCPU', {
      alarmName: `EC2-HighCPU-${props.instanceId}`,
      alarmDescription: 'EC2 instance CPU utilization is high',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/EC2',
        metricName: 'CPUUtilization',
        dimensionsMap: {
          InstanceId: props.instanceId,
        },
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 80,
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highCpuAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 2. StatusCheckFailed(ステータスチェック失敗)
    const statusCheckAlarm = new cloudwatch.Alarm(this, 'EC2StatusCheckFailed', {
      alarmName: `EC2-StatusCheckFailed-${props.instanceId}`,
      alarmDescription: 'EC2 instance status check failed',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/EC2',
        metricName: 'StatusCheckFailed',
        dimensionsMap: {
          InstanceId: props.instanceId,
        },
        statistic: 'Maximum',
        period: cdk.Duration.minutes(1),
      }),
      threshold: 1,
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
    });
    statusCheckAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 3. メモリ使用率(CloudWatch Agent必要)
    const highMemoryAlarm = new cloudwatch.Alarm(this, 'EC2HighMemory', {
      alarmName: `EC2-HighMemory-${props.instanceId}`,
      alarmDescription: 'EC2 instance memory utilization is high',
      metric: new cloudwatch.Metric({
        namespace: 'CWAgent',
        metricName: 'mem_used_percent',
        dimensionsMap: {
          InstanceId: props.instanceId,
        },
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 80,
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highMemoryAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 4. ディスク使用率(CloudWatch Agent必要)
    const highDiskAlarm = new cloudwatch.Alarm(this, 'EC2HighDisk', {
      alarmName: `EC2-HighDisk-${props.instanceId}`,
      alarmDescription: 'EC2 instance disk utilization is high',
      metric: new cloudwatch.Metric({
        namespace: 'CWAgent',
        metricName: 'disk_used_percent',
        dimensionsMap: {
          InstanceId: props.instanceId,
          path: '/',
          fstype: 'ext4',
        },
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 80,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highDiskAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));
  }
}

5.5 RDSアラーム

// lib/alarms/rds-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 rds from 'aws-cdk-lib/aws-rds';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';

interface RdsAlarmsProps {
  dbInstanceIdentifier: string;
  alarmTopic: sns.ITopic;
}

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

    // RDSインスタンスの参照取得
    const dbInstance = rds.DatabaseInstance.fromDatabaseInstanceAttributes(
      this,
      'Database',
      {
        instanceIdentifier: props.dbInstanceIdentifier,
        instanceEndpointAddress: '', // ダミー値
        port: 5432,
        securityGroups: [],
      }
    );

    // 1. DatabaseConnections(接続数)
    const highConnectionsAlarm = new cloudwatch.Alarm(this, 'RDSHighConnections', {
      alarmName: `RDS-HighConnections-${props.dbInstanceIdentifier}`,
      alarmDescription: 'RDS database connections are high',
      metric: dbInstance.metricDatabaseConnections({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 320, // db.t3.mediumの80%(要調整)
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highConnectionsAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 2. CPUUtilization(CPU使用率)
    const highCpuAlarm = new cloudwatch.Alarm(this, 'RDSHighCPU', {
      alarmName: `RDS-HighCPU-${props.dbInstanceIdentifier}`,
      alarmDescription: 'RDS CPU utilization is high',
      metric: dbInstance.metricCPUUtilization({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 80,
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highCpuAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 3. FreeableMemory(空きメモリ)
    const lowMemoryAlarm = new cloudwatch.Alarm(this, 'RDSLowMemory', {
      alarmName: `RDS-LowMemory-${props.dbInstanceIdentifier}`,
      alarmDescription: 'RDS freeable memory is low',
      metric: dbInstance.metricFreeableMemory({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 100 * 1024 * 1024, // 100MB
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
    });
    lowMemoryAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 4. ReadLatency(読み取りレイテンシ)
    const highReadLatencyAlarm = new cloudwatch.Alarm(this, 'RDSHighReadLatency', {
      alarmName: `RDS-HighReadLatency-${props.dbInstanceIdentifier}`,
      alarmDescription: 'RDS read latency is high',
      metric: dbInstance.metricReadLatency({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 0.01, // 10ms
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highReadLatencyAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 5. WriteLatency(書き込みレイテンシ)
    const highWriteLatencyAlarm = new cloudwatch.Alarm(this, 'RDSHighWriteLatency', {
      alarmName: `RDS-HighWriteLatency-${props.dbInstanceIdentifier}`,
      alarmDescription: 'RDS write latency is high',
      metric: dbInstance.metricWriteLatency({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 0.01, // 10ms
      evaluationPeriods: 2,
      datapointsToAlarm: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });
    highWriteLatencyAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));

    // 6. FreeStorageSpace(空きストレージ)
    const lowStorageAlarm = new cloudwatch.Alarm(this, 'RDSLowStorage', {
      alarmName: `RDS-LowStorage-${props.dbInstanceIdentifier}`,
      alarmDescription: 'RDS free storage space is low',
      metric: dbInstance.metricFreeStorageSpace({
        statistic: 'Average',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 5 * 1024 * 1024 * 1024, // 5GB
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
    });
    lowStorageAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));
  }
}

5.6 統合ダッシュボード

// lib/dashboard/monitoring-dashboard.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';

interface MonitoringDashboardProps {
  albArn: string;
  instanceId: string;
  dbInstanceIdentifier: string;
}

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

    const dashboard = new cloudwatch.Dashboard(this, 'Dashboard', {
      dashboardName: 'WebAppMonitoring',
    });

    // ALBセクション
    dashboard.addWidgets(
      new cloudwatch.TextWidget({
        markdown: '# ALB Metrics',
        width: 24,
        height: 1,
      })
    );

    dashboard.addWidgets(
      new cloudwatch.GraphWidget({
        title: 'ALB Target Response Time',
        left: [
          new cloudwatch.Metric({
            namespace: 'AWS/ApplicationELB',
            metricName: 'TargetResponseTime',
            dimensionsMap: {
              LoadBalancer: this.extractAlbName(props.albArn),
            },
            statistic: 'Average',
          }),
        ],
        width: 12,
      }),
      new cloudwatch.GraphWidget({
        title: 'ALB 5XX Errors',
        left: [
          new cloudwatch.Metric({
            namespace: 'AWS/ApplicationELB',
            metricName: 'HTTPCode_Target_5XX_Count',
            dimensionsMap: {
              LoadBalancer: this.extractAlbName(props.albArn),
            },
            statistic: 'Sum',
          }),
        ],
        width: 12,
      })
    );

    // EC2セクション
    dashboard.addWidgets(
      new cloudwatch.TextWidget({
        markdown: '# EC2 Metrics',
        width: 24,
        height: 1,
      })
    );

    dashboard.addWidgets(
      new cloudwatch.GraphWidget({
        title: 'EC2 CPU Utilization',
        left: [
          new cloudwatch.Metric({
            namespace: 'AWS/EC2',
            metricName: 'CPUUtilization',
            dimensionsMap: {
              InstanceId: props.instanceId,
            },
            statistic: 'Average',
          }),
        ],
        width: 12,
      }),
      new cloudwatch.GraphWidget({
        title: 'EC2 Memory Utilization',
        left: [
          new cloudwatch.Metric({
            namespace: 'CWAgent',
            metricName: 'mem_used_percent',
            dimensionsMap: {
              InstanceId: props.instanceId,
            },
            statistic: 'Average',
          }),
        ],
        width: 12,
      })
    );

    // RDSセクション
    dashboard.addWidgets(
      new cloudwatch.TextWidget({
        markdown: '# RDS Metrics',
        width: 24,
        height: 1,
      })
    );

    dashboard.addWidgets(
      new cloudwatch.GraphWidget({
        title: 'RDS Database Connections',
        left: [
          new cloudwatch.Metric({
            namespace: 'AWS/RDS',
            metricName: 'DatabaseConnections',
            dimensionsMap: {
              DBInstanceIdentifier: props.dbInstanceIdentifier,
            },
            statistic: 'Average',
          }),
        ],
        width: 12,
      }),
      new cloudwatch.GraphWidget({
        title: 'RDS CPU Utilization',
        left: [
          new cloudwatch.Metric({
            namespace: 'AWS/RDS',
            metricName: 'CPUUtilization',
            dimensionsMap: {
              DBInstanceIdentifier: props.dbInstanceIdentifier,
            },
            statistic: 'Average',
          }),
        ],
        width: 12,
      })
    );
  }

  private extractAlbName(albArn: string): string {
    // ARNからALB名を抽出
    // arn:aws:elasticloadbalancing:region:account-id:loadbalancer/app/load-balancer-name/load-balancer-id
    const parts = albArn.split(':');
    const namePart = parts[parts.length - 1];
    return namePart.split('/').slice(1).join('/');
  }
}

コスト最適化のテクニック

監視を実装する際、コストを抑えるためのテクニックを紹介します。

1. アラーム数の最適化

不要なアラームを削除:

  • 実際にアクションを取らないアラームは削除
  • 類似のアラームは統合(例: 複数のしきい値ではなく、1つの適切なしきい値)

コスト:

  • CloudWatchアラーム: $0.10/月/アラーム
  • 10個のアラーム = $1/月
  • 100個のアラーム = $10/月

2. メトリクスの保持期間

CloudWatchメトリクスは自動的に以下の保持期間で保存されます:

  • 1分間隔: 15日間
  • 5分間隔: 63日間
  • 1時間間隔: 455日間

古いデータは自動的に削除されるため、追加コストはかかりません。

3. カスタムメトリクスの頻度調整

CloudWatch Agentで取得するカスタムメトリクス(メモリ、ディスク)は、頻度を調整できます。

{
  "metrics": {
    "metrics_collected": {
      "mem": {
        "measurement": [
          {
            "name": "mem_used_percent",
            "rename": "mem_used_percent",
            "unit": "Percent"
          }
        ],
        "metrics_collection_interval": 60
      }
    }
  }
}
  • リアルタイム監視: 60秒(1分)
  • コスト重視: 300秒(5分)

コスト:

  • カスタムメトリクス: $0.30/月/メトリクス(最初の10,000メトリクス)

4. ログの保持期間設定

CloudWatch Logsの保持期間を適切に設定します。

new logs.LogGroup(this, 'ApplicationLogs', {
  logGroupName: '/aws/application/logs',
  retention: logs.RetentionDays.ONE_WEEK, // 1週間
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});
  • 本番環境: 30日〜90日
  • 開発環境: 1週間〜14日

まとめ

本記事では、「何を監視すべきか」という課題に対して、理論と実践の2つのアプローチを組み合わせて解説しました。

重要なポイント

  1. すべてを監視する必要はない

    • Four Golden Signals(Latency、Traffic、Errors、Saturation)を基準に
    • ビジネスへの影響度で優先順位をつける
  2. AWS公式の推奨アラームを活用

    • CloudWatchが提供する推奨アラームは、AWSの運用ノウハウの結晶
    • しきい値も事前設定されており、すぐに使える
  3. 各レイヤーで監視すべきメトリクス

    • ALB: TargetResponseTime、HTTPCode_Target_5XX_Count、HealthyHostCount
    • EC2: CPUUtilization、StatusCheckFailed、メモリ・ディスク使用率
    • RDS: DatabaseConnections、CPUUtilization、FreeableMemory、レイテンシ

次のステップ

監視設計は、一度設定したら終わりではありません。以下のサイクルで継続的に改善していきましょう:

  1. デプロイ: CDKで監視基盤を構築
  2. 観察: アラートの発生頻度を確認
  3. 調整: 誤検知が多ければしきい値を調整
  4. 改善: 見逃しがあればメトリクスを追加

監視は、システムの健全性を維持し、ユーザー体験を向上させるための重要な要素です。本記事の内容が、皆さんの監視設計の一助となれば幸いです。


参考リンク:

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?