はじめに
前回の記事「何を監視すべきか迷わないために - AWS CloudWatchで実装するWebアプリ監視設計パターン」では、シンプルなEC2ベースの3層構成(ALB + EC2 + RDS)における監視設計を解説しました。
しかし、現在多くのシステムがEC2からコンテナ(ECS Fargate)への移行を進めています。コンテナ化には、デプロイの柔軟性、リソースの効率化、インフラ管理の簡素化といったメリットがありますが、監視設計においては新たな考慮事項が生まれます。
本記事では、**ECS Fargateを採用したコンテナ構成(ALB + ECS Fargate + RDS)**における監視設計を、EC2構成との違いを明確にしながら解説します。
本記事で扱う課題
コンテナ構成での監視を設定しようとしたときに直面する悩み:
- 「EC2の監視設計は理解しているが、コンテナになると何が変わるのか?」
- 「インスタンスではなく、タスクやコンテナを監視するとはどういうことか?」
- 「Container Insightsは有効化すべき?追加コストは?」
- 「タスクが動的に増減する環境で、何をアラート対象にすべきか?」
本記事では、これらの疑問に答えながら、EC2構成との対比とContainer Insightsの活用方法を中心に、実践的な監視設計を紹介します。
本記事の焦点
ALBとRDSの監視は前回記事とほぼ同じため、本記事ではECS FargateとContainer Insightsに焦点を当てて解説します。
- ALB・RDS: 前回記事の監視設計を踏襲(本記事では簡潔に記載)
-
ECS Fargate: 本記事のメインコンテンツ(詳細に解説)
- タスクレベルのCPU/メモリ監視
- タスク数の監視(DesiredTaskCount vs RunningTaskCount)
- Container Insightsの活用方法とコスト
- EC2構成からの移行ポイント
まずは結論
Fargate構成で特に重要なメトリクスはこれ!
ECSタスク層:
- CPUUtilization(タスクレベル): タスク定義の適切性を判断
- MemoryUtilization(タスクレベル): OOM Killerによる強制終了を防ぐ
- RunningTaskCount: タスクの可用性を監視
- DesiredTaskCount vs RunningTaskCount: タスク起動失敗を検知
ALB層とRDS層:
- 基本的にEC2構成と同じ監視設計を適用可能
Container Insightsを活用しよう
メリット:
- 標準で豊富なメトリクスを取得可能
- CloudWatch Agentのような設定不要
- 自動ダッシュボードで全体像を把握
注意点:
- 追加コストが発生(1タスクあたり月額約$0.30〜$1.00)
- 環境ごとに有効化/無効化を使い分ける
コンテナ監視をする利点とは
- より細かい粒度: タスクレベルでの監視により、問題の特定が容易
- 標準化されたログ収集: タスク定義で簡単に設定
- 自動スケーリングとの連携: メトリクスベースの迅速なスケーリング
- インフラ管理の簡素化: CloudWatch Agentの管理が不要
構成とEC2構成との違い
2.1 アーキテクチャの比較
まず、EC2構成とFargate構成のアーキテクチャを比較します。
EC2構成(前回記事)
Internet
↓
[ALB] ← ロードバランサー層
↓
[EC2インスタンス] ← アプリケーション層(固定されたインスタンス)
↓
[RDS] ← データベース層
特徴:
- EC2インスタンスが常時稼働
- インスタンスレベルでのリソース管理
- Auto Scalingでインスタンス単位でスケール
Fargate構成(今回)
Internet
↓
[ALB] ← ロードバランサー層
↓
[ECS Fargate タスク] ← アプリケーション層(動的なコンテナタスク)
↓ ↑
↓ (タスクが動的に増減)
[RDS] ← データベース層
特徴:
- ECSタスク(コンテナ)が動的に起動・停止
- タスクレベルでのリソース管理(CPU/メモリをタスク定義で指定)
- より細かい粒度でのスケーリング
2.2 監視における主要な違い
| 観点 | EC2構成 | Fargate構成 |
|---|---|---|
| 監視単位 | EC2インスタンス | ECSタスク(コンテナ) |
| メトリクス取得 | CloudWatch基本メトリクス + Agent(メモリ・ディスク) |
Container Insights (標準で豊富なメトリクス) |
| リソース管理 | インスタンスタイプで固定 | タスク定義でCPU/メモリ指定 |
| スケーリング対象 | インスタンス数 | タスク数(より細かい) |
| ログ収集 | CloudWatch Logs Agent設定 | awslogsログドライバー(標準) |
| 動的性 | 比較的静的 | タスクが頻繁に起動・停止 |
| インフラ管理 | OS・ミドルウェア管理が必要 | マネージド(サーバーレス) |
2.3 コンテナ構成特有の監視課題
Fargate構成では、以下の点が新たな監視の考慮事項となります。
1. タスクのライフサイクル管理
- タスクは動的に起動・停止されるため、「期待するタスク数」と「実際のタスク数」のギャップを監視する必要がある
- タスク起動失敗やリソース不足による起動遅延を検知
2. タスクレベルのリソース管理
- インスタンス全体ではなく、タスクごとにCPU/メモリが割り当てられる
- タスク定義で指定したリソースが適切かを継続的に評価
3. Container Insightsの活用
- EC2のCloudWatch Agentに相当するが、標準で有効化可能
- ただし、追加コストが発生するため、コストとメトリクスのトレードオフを考慮
監視設計の基本原則
前回の記事と同様、監視設計には2つのアプローチを組み合わせます。
3.1 理論的フレームワーク:Four Golden Signals
Google SREが提唱するFour Golden Signalsは、コンテナ構成でも変わらず有効です。
1. Latency(レイテンシ)
- リクエストの応答時間
- コンテナ特有の観点: タスクのCPU/メモリ不足がレイテンシに直結しやすい
2. Traffic(トラフィック)
- システムへの需要(リクエスト数、タスク数)
- コンテナ特有の観点: トラフィック増加に応じたタスクのスケーリング状況を監視
3. Errors(エラー)
- 失敗したリクエストの割合
- コンテナ特有の観点: タスクの起動失敗、OOM Killerによるタスク終了
4. Saturation(飽和度)
- リソースの使用率
- コンテナ特有の観点: タスク定義で指定したCPU/メモリの使用率、クラスタのリソース制約
3.2 AWS公式ベストプラクティス
AWSは、ECS/Fargateにおいても推奨アラームを提供しています。加えて、Container Insightsにより、コンテナ特有のメトリクスを標準で取得できます。
本記事では、これらを組み合わせた実践的な監視設計を提示します。
各レイヤーの監視設計
4.1 ALB(ロードバランサー層)
ALBは、EC2構成とFargate構成で基本的な監視設計は同じです。前回記事で詳しく解説した以下の4つのメトリクスを使用します。
必須メトリクス
前回記事と同じメトリクスを監視します:
- TargetResponseTime(ターゲット応答時間) - Latency
- HTTPCode_Target_5XX_Count(5xxエラー数) - Errors
- HealthyHostCount(正常なホスト数) - Saturation + Errors
- RequestCount(リクエスト数) - Traffic
メトリクスの詳細、しきい値の考え方、CDKコード例は前回記事を参照してください。
Fargate構成での注意点
バックエンドがECSタスクになることで、以下の点に注意が必要です:
TargetResponseTime(レイテンシ):
- タスクのCPU/メモリ不足は、EC2に比べてより顕著にレイテンシに影響
- タスク定義のリソース設定が不適切な場合、レスポンスが急激に遅延
HTTPCode_Target_5XX_Count(エラー):
- タスクがメモリ不足でOOM Killerに強制終了されると、5xxエラーが発生
- タスク起動失敗時にもエラーが増加
HealthyHostCount(ヘルスチェック):
- タスクは動的に起動・停止されるため、一時的な減少は正常な場合もある
- DesiredTaskCount(後述)と比較して、タスク起動失敗を検知することが重要
RequestCount(トラフィック):
- タスクのスケーリングは、EC2のAuto Scalingより細かく迅速に実行される
- トラフィック急増時のタスク起動状況を監視
4.2 ECS Fargate(コンテナ層)
ここが今回のメインコンテンツです。EC2構成との違いを明確にしながら、ECS Fargateの監視設計を詳しく解説します。
必須メトリクス
1. CPUUtilization(タスクレベル)
何を測定するか:
タスク定義で指定したCPUユニットに対する使用率(0〜100%)。
なぜ重要か:
- タスクのリソース飽和度を示す(Saturation)
- 高CPU使用率が継続すると、レスポンス遅延
- タスク定義のCPU設定が適切かを判断
EC2構成との違い:
- EC2: インスタンス全体のCPU使用率を監視(複数のプロセスが共存)
- Fargate: タスクごとのCPU使用率を監視(タスク定義で指定した値が上限)
- メリット: タスクごとに独立して監視できるため、問題の特定が容易
しきい値の考え方:
- タスク定義のCPU設定が適切であれば、70%以下が理想
- 警告: 80%以上が5分以上継続
- 緊急: 90%以上(タスク定義の見直しまたはスケールアウト)
CDKコード例:
new cloudwatch.Alarm(this, 'ECSHighCPU', {
metric: new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'CPUUtilization',
dimensionsMap: {
ServiceName: serviceName,
ClusterName: clusterName,
},
statistic: 'Average',
period: cdk.Duration.minutes(5),
}),
threshold: 80,
evaluationPeriods: 2,
datapointsToAlarm: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
2. MemoryUtilization(タスクレベル)
何を測定するか:
タスク定義で指定したメモリに対する使用率(0〜100%)。
なぜ重要か:
- メモリ枯渇を検知(Saturation)
- メモリ使用率が100%に達すると、OOM Killerがタスクを強制終了
- タスク定義のメモリ設定が適切かを判断
EC2構成との違い:
-
EC2: CloudWatch Agentのインストールが必要(
mem_used_percent) - Fargate: 標準メトリクスとして取得可能(追加設定不要)
- 重要な違い: Fargateでは100%到達でタスクが強制終了される(EC2はスワップがある)
しきい値の考え方:
- 警告: 80%以上(OOMリスクが高まる)
- 緊急: 90%以上(即座にタスク定義の見直しが必要)
CDKコード例:
new cloudwatch.Alarm(this, 'ECSHighMemory', {
metric: new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'MemoryUtilization',
dimensionsMap: {
ServiceName: serviceName,
ClusterName: clusterName,
},
statistic: 'Average',
period: cdk.Duration.minutes(5),
}),
threshold: 80,
evaluationPeriods: 2,
datapointsToAlarm: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
3. RunningTaskCount(サービスレベル)
何を測定するか:
ECSサービスで実行中のタスク数。
なぜ重要か:
- サービスの可用性を示す(Saturation + Errors)
- タスクが期待通りに起動しているかを確認
- DesiredTaskCountと比較して、タスク起動失敗を検知
EC2構成との違い:
- EC2: EC2インスタンス数を監視(比較的固定的)
- Fargate: タスク数を監視(動的に変化)
- 新しい概念: DesiredTaskCountとRunningTaskCountのギャップが重要
しきい値の考え方:
- 最小: 2タスク以上(冗長性の確保)
- 警告: DesiredTaskCountの50%以下
- 緊急: 1タスク以下(単一障害点)
CDKコード例:
// ECSサービスのメトリクスを直接使用
new cloudwatch.Alarm(this, 'ECSLowTaskCount', {
metric: service.metricRunningTaskCount({
statistic: 'Average',
period: cdk.Duration.minutes(1),
}),
threshold: 2,
evaluationPeriods: 1,
comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
});
4. DesiredTaskCount vs RunningTaskCount(ギャップ監視)
何を測定するか:
期待するタスク数(DesiredTaskCount)と実際に実行中のタスク数(RunningTaskCount)の差分。
なぜ重要か:
- タスク起動失敗を検知(Errors)
- リソース不足(ECSクラスタの容量制限)やタスク定義の問題を特定
- タスクのクラッシュループを検知
EC2構成との違い:
- EC2: インスタンス起動は時間がかかるため、ギャップは許容される
- Fargate: タスク起動は比較的速いため、ギャップが長時間続くのは異常
しきい値の考え方:
- 警告: DesiredとRunningの差が2以上で5分以上継続
- 緊急: DesiredとRunningの差が50%以上
CDKコード例:
// Math式を使ったギャップ監視
const desiredMetric = service.metricDesiredTaskCount();
const runningMetric = service.metricRunningTaskCount();
new cloudwatch.Alarm(this, 'ECSTaskCountGap', {
metric: new cloudwatch.MathExpression({
expression: 'desired - running',
usingMetrics: {
desired: desiredMetric,
running: runningMetric,
},
}),
threshold: 2,
evaluationPeriods: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
Container Insightsの追加メトリクス
Container Insightsを有効化すると、より詳細なコンテナレベルのメトリクスが取得できます。
5. task_cpu_utilization(Container Insights)
何を測定するか:
コンテナごとのCPU使用率(より詳細)。
なぜ重要か:
- 標準のCPUUtilizationはサービス全体の平均
- Container Insightsでは、タスクごと、コンテナごとの詳細な使用率を取得
EC2構成との違い:
- EC2: CloudWatch Agentでプロセスごとのメトリクスを取得可能だが、設定が複雑
- Fargate: Container Insightsで標準的に取得可能
CDKコード例:
new cloudwatch.Alarm(this, 'ContainerHighCPU', {
metric: new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'CpuUtilized',
dimensionsMap: {
ServiceName: serviceName,
ClusterName: clusterName,
},
statistic: 'Average',
}),
threshold: 200, // CPU Units(タスク定義で256の場合、200は約78%)
evaluationPeriods: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
6. task_memory_utilization(Container Insights)
何を測定するか:
コンテナごとのメモリ使用率(より詳細)。
なぜ重要か:
- 標準のMemoryUtilizationは割合(%)
- Container Insightsでは、実際の使用量(MB)も取得可能
CDKコード例:
new cloudwatch.Alarm(this, 'ContainerHighMemory', {
metric: new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'MemoryUtilized',
dimensionsMap: {
ServiceName: serviceName,
ClusterName: clusterName,
},
statistic: 'Average',
}),
threshold: 400, // MB(タスク定義で512MBの場合、400は約78%)
evaluationPeriods: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
7. NetworkRxBytes / NetworkTxBytes(Container Insights)
何を測定するか:
コンテナのネットワークトラフィック(受信/送信バイト数)。
なぜ重要か:
- ネットワークI/Oの傾向を把握(Traffic)
- 異常なトラフィック増加を検知
EC2構成との違い:
- EC2: インスタンス全体のネットワークメトリクスのみ
- Fargate: タスクごとのネットワークトラフィックを監視
CDKコード例:
// 監視よりもダッシュボードでの傾向確認が主用途
new cloudwatch.GraphWidget({
title: 'ECS Network Traffic',
left: [
new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'NetworkRxBytes',
dimensionsMap: {
ServiceName: serviceName,
ClusterName: clusterName,
},
statistic: 'Sum',
}),
new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'NetworkTxBytes',
dimensionsMap: {
ServiceName: serviceName,
ClusterName: clusterName,
},
statistic: 'Sum',
}),
],
});
4.3 RDS(データベース層)
RDSの監視は、EC2構成とFargate構成でほぼ同じです。接続元がEC2インスタンスかECSタスクかの違いだけで、監視設計は変わりません。
必須メトリクス
前回記事と同じメトリクスを監視します:
- DatabaseConnections(データベース接続数) - Saturation
- CPUUtilization(CPU使用率) - Saturation
- FreeableMemory(空きメモリ) - Saturation
- ReadLatency / WriteLatency(読み取り/書き込みレイテンシ) - Latency
- FreeStorageSpace(空きストレージ容量) - Saturation
メトリクスの詳細、しきい値の考え方、CDKコード例は前回記事を参照してください。
Fargate構成での注意点
DatabaseConnections:
- タスクは動的に起動・停止されるため、コネクションプールの管理が重要
- タスクがスケールインする際、コネクションの適切なクローズが必要
- タスク数の増減が頻繁な場合、コネクション数の変動も大きくなる
Container Insightsの設定と活用
Container Insightsは、ECS Fargateにおける監視の中核となる機能です。
5.1 Container Insightsとは
AWS公式のコンテナ監視ソリューションで、以下を提供します:
- タスクレベルのメトリクス: CPU、メモリ、ネットワーク、ディスクI/O
- コンテナレベルのメトリクス: 各コンテナの詳細なリソース使用状況
- サービスレベルのメトリクス: サービス全体の集計メトリクス
- 自動ダッシュボード: CloudWatchコンソールに自動生成される統合ダッシュボード
5.2 有効化方法
Container Insightsは、ECSクラスタ作成時に有効化できます。
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
clusterName: 'my-cluster',
containerInsights: true, // Container Insightsを有効化
});
既存のクラスタで有効化する場合:
aws ecs update-cluster-settings \
--cluster my-cluster \
--settings name=containerInsights,value=enabled
5.3 取得できるメトリクス
Container Insightsで取得できる主要なメトリクス:
| メトリクス | 説明 | 名前空間 | 単位 |
|---|---|---|---|
| CpuUtilized | 使用中のCPUユニット | ECS/ContainerInsights | Count |
| CpuReserved | 予約されたCPUユニット | ECS/ContainerInsights | Count |
| MemoryUtilized | 使用中のメモリ | ECS/ContainerInsights | MB |
| MemoryReserved | 予約されたメモリ | ECS/ContainerInsights | MB |
| NetworkRxBytes | 受信バイト数 | ECS/ContainerInsights | Bytes |
| NetworkTxBytes | 送信バイト数 | ECS/ContainerInsights | Bytes |
| StorageReadBytes | ストレージ読み取り | ECS/ContainerInsights | Bytes |
| StorageWriteBytes | ストレージ書き込み | ECS/ContainerInsights | Bytes |
5.4 ディメンション(集計レベル)
Container Insightsのメトリクスは、以下のディメンションで集計できます:
- ClusterName: クラスタ全体の集計
- ServiceName: サービスごとの集計
- TaskDefinitionFamily: タスク定義ファミリーごとの集計
- ContainerName: コンテナごとの詳細
5.5 コスト
Container Insightsは追加料金が発生します。
料金体系:
- カスタムメトリクスとして課金(APIリクエスト + メトリクス保存)
- 1タスクあたり月額約$0.30〜$1.00(メトリクス数による)
- 10タスクの場合: 月額約$3〜$10
コスト最適化の考え方:
- 本番環境では有効化推奨(詳細な監視が重要)
- 開発環境では無効化を検討(基本メトリクスで十分な場合)
- 必要なメトリクスのみアラーム設定(すべてのメトリクスをアラート対象にしない)
5.6 EC2構成(CloudWatch Agent)との比較
| 項目 | EC2 + CloudWatch Agent | Fargate + Container Insights |
|---|---|---|
| 設定の複雑さ | Agentのインストール・設定が必要 | クラスタ作成時に有効化のみ |
| メトリクスの詳細度 | インスタンスレベル + プロセスレベル | タスクレベル + コンテナレベル |
| メモリ監視 | Agent必須 | 標準メトリクスで取得可能 |
| ディスク監視 | Agent必須 | Container Insightsで取得可能 |
| コスト | カスタムメトリクス課金 | カスタムメトリクス課金 |
| 保守性 | Agent更新が必要 | マネージド(自動更新) |
CDKによる統合実装
ここまでの監視設計をTypeScript CDKで実装します。プロジェクト構造は前回記事と同様ですが、EC2アラームをECSアラームに置き換えます。
6.1 プロジェクト構造
container-monitoring-stack/
├── bin/
│ └── app.ts
├── lib/
│ ├── monitoring-stack.ts # メインスタック
│ ├── alarms/
│ │ ├── alb-alarms.ts # ALBアラーム(前回とほぼ同じ)
│ │ ├── ecs-alarms.ts # ECSアラーム(新規)
│ │ └── rds-alarms.ts # RDSアラーム(前回とほぼ同じ)
│ └── dashboard/
│ └── monitoring-dashboard.ts # 統合ダッシュボード
├── package.json
└── tsconfig.json
6.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 { EcsAlarms } from './alarms/ecs-alarms';
import { RdsAlarms } from './alarms/rds-alarms';
import { MonitoringDashboard } from './dashboard/monitoring-dashboard';
interface MonitoringStackProps extends cdk.StackProps {
albArn: string;
targetGroupArn: string;
clusterName: string;
serviceName: 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: 'Container Application Monitoring Alarms',
topicName: 'container-app-monitoring-alarms',
});
// メール通知の設定
alarmTopic.addSubscription(
new subscriptions.EmailSubscription(props.alertEmail)
);
// ALBアラームの作成
new AlbAlarms(this, 'AlbAlarms', {
albArn: props.albArn,
targetGroupArn: props.targetGroupArn,
alarmTopic,
});
// ECSアラームの作成
new EcsAlarms(this, 'EcsAlarms', {
clusterName: props.clusterName,
serviceName: props.serviceName,
alarmTopic,
});
// RDSアラームの作成
new RdsAlarms(this, 'RdsAlarms', {
dbInstanceIdentifier: props.dbInstanceIdentifier,
alarmTopic,
});
// 統合ダッシュボードの作成
new MonitoringDashboard(this, 'Dashboard', {
albArn: props.albArn,
clusterName: props.clusterName,
serviceName: props.serviceName,
dbInstanceIdentifier: props.dbInstanceIdentifier,
});
// 出力
new cdk.CfnOutput(this, 'AlarmTopicArn', {
value: alarmTopic.topicArn,
description: 'SNS Topic ARN for alarms',
});
}
}
6.3 ALBアラーム
ALBアラームは前回記事と同じ実装を使用します。詳細な実装は前回記事を参照してください。
前回記事のalb-alarms.tsをそのまま使用できます:
- TargetResponseTime(レイテンシ)
- HTTPCode_Target_5XX_Count(5xxエラー)
- HealthyHostCount(正常なホスト数)
- RequestCount(リクエスト数)
6.4 ECSアラーム(詳細)
// lib/alarms/ecs-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 ecs from 'aws-cdk-lib/aws-ecs';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Construct } from 'constructs';
interface EcsAlarmsProps {
clusterName: string;
serviceName: string;
alarmTopic: sns.ITopic;
// Container Insightsのメトリクスも使用する場合
enableContainerInsightsAlarms?: boolean;
}
export class EcsAlarms extends Construct {
constructor(scope: Construct, id: string, props: EcsAlarmsProps) {
super(scope, id);
// ECSサービスの参照取得
const cluster = ecs.Cluster.fromClusterAttributes(this, 'Cluster', {
clusterName: props.clusterName,
vpc: undefined as any, // 監視のみなのでダミー値
securityGroups: [],
});
const service = ecs.FargateService.fromFargateServiceAttributes(
this,
'Service',
{
cluster,
serviceName: props.serviceName,
}
);
// 1. CPUUtilization(タスクレベル)
const highCpuAlarm = new cloudwatch.Alarm(this, 'ECSHighCPU', {
alarmName: `ECS-HighCPU-${props.serviceName}`,
alarmDescription: 'ECS service CPU utilization is high',
metric: new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'CPUUtilization',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
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. MemoryUtilization(タスクレベル)
const highMemoryAlarm = new cloudwatch.Alarm(this, 'ECSHighMemory', {
alarmName: `ECS-HighMemory-${props.serviceName}`,
alarmDescription: 'ECS service memory utilization is high (OOM risk)',
metric: new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'MemoryUtilization',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
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));
// 3. RunningTaskCount(実行中のタスク数)
const lowTaskCountAlarm = new cloudwatch.Alarm(this, 'ECSLowTaskCount', {
alarmName: `ECS-LowTaskCount-${props.serviceName}`,
alarmDescription: 'ECS service has insufficient running tasks',
metric: service.metricRunningTaskCount({
statistic: 'Average',
period: cdk.Duration.minutes(1),
}),
threshold: 2,
evaluationPeriods: 1,
datapointsToAlarm: 1,
comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
});
lowTaskCountAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));
// 4. DesiredTaskCount vs RunningTaskCount(ギャップ監視)
const taskCountGapAlarm = new cloudwatch.Alarm(this, 'ECSTaskCountGap', {
alarmName: `ECS-TaskCountGap-${props.serviceName}`,
alarmDescription: 'Gap between desired and running tasks (task launch failure)',
metric: new cloudwatch.MathExpression({
expression: 'desired - running',
usingMetrics: {
desired: service.metricDesiredTaskCount({
statistic: 'Average',
period: cdk.Duration.minutes(1),
}),
running: service.metricRunningTaskCount({
statistic: 'Average',
period: cdk.Duration.minutes(1),
}),
},
period: cdk.Duration.minutes(1),
}),
threshold: 2,
evaluationPeriods: 2,
datapointsToAlarm: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
taskCountGapAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(props.alarmTopic));
// Container Insightsのアラーム(オプション)
if (props.enableContainerInsightsAlarms) {
// 5. CpuUtilized(Container Insights)
const containerHighCpuAlarm = new cloudwatch.Alarm(
this,
'ContainerHighCPU',
{
alarmName: `Container-HighCPU-${props.serviceName}`,
alarmDescription: 'Container CPU utilization is high (Container Insights)',
metric: new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'CpuUtilized',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
statistic: 'Average',
period: cdk.Duration.minutes(5),
}),
threshold: 200, // CPU Units(タスク定義に応じて調整)
evaluationPeriods: 2,
datapointsToAlarm: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
}
);
containerHighCpuAlarm.addAlarmAction(
new cloudwatch_actions.SnsAction(props.alarmTopic)
);
// 6. MemoryUtilized(Container Insights)
const containerHighMemoryAlarm = new cloudwatch.Alarm(
this,
'ContainerHighMemory',
{
alarmName: `Container-HighMemory-${props.serviceName}`,
alarmDescription:
'Container memory utilization is high (Container Insights)',
metric: new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'MemoryUtilized',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
statistic: 'Average',
period: cdk.Duration.minutes(5),
}),
threshold: 400, // MB(タスク定義に応じて調整)
evaluationPeriods: 2,
datapointsToAlarm: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
}
);
containerHighMemoryAlarm.addAlarmAction(
new cloudwatch_actions.SnsAction(props.alarmTopic)
);
}
}
}
6.5 RDSアラーム
RDSアラームは前回記事と同じ実装を使用します。詳細な実装は前回記事を参照してください。
前回記事のrds-alarms.tsをそのまま使用できます:
- DatabaseConnections(データベース接続数)
- CPUUtilization(CPU使用率)
- FreeableMemory(空きメモリ)
- ReadLatency / WriteLatency(読み取り/書き込みレイテンシ)
- FreeStorageSpace(空きストレージ容量)
6.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;
clusterName: string;
serviceName: 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: 'ContainerAppMonitoring',
});
// 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,
})
);
// ECSセクション
dashboard.addWidgets(
new cloudwatch.TextWidget({
markdown: '# ECS Fargate Metrics',
width: 24,
height: 1,
})
);
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'ECS Task Count',
left: [
new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'DesiredTaskCount',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
statistic: 'Average',
label: 'Desired',
}),
new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'RunningTaskCount',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
statistic: 'Average',
label: 'Running',
}),
],
width: 12,
}),
new cloudwatch.GraphWidget({
title: 'ECS CPU Utilization',
left: [
new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'CPUUtilization',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
statistic: 'Average',
}),
],
width: 12,
})
);
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'ECS Memory Utilization',
left: [
new cloudwatch.Metric({
namespace: 'AWS/ECS',
metricName: 'MemoryUtilization',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
statistic: 'Average',
}),
],
width: 12,
}),
new cloudwatch.GraphWidget({
title: 'Container Insights - CPU Utilized',
left: [
new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'CpuUtilized',
dimensionsMap: {
ServiceName: props.serviceName,
ClusterName: props.clusterName,
},
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 {
const parts = albArn.split(':');
const namePart = parts[parts.length - 1];
return namePart.split('/').slice(1).join('/');
}
}
EC2構成からの移行ポイント
既存のEC2構成からFargate構成に移行する際の監視設計の変更点をまとめます。
7.1 監視設計の変更点
| 項目 | EC2構成 | Fargate構成 | 移行アクション |
|---|---|---|---|
| CPU/メモリ監視 | インスタンス全体 | タスクごと | タスク定義のリソース設定を監視 |
| メトリクス取得 | CloudWatch Agent必要 | Container Insights(標準) | Agent削除、Container Insights有効化 |
| スケーリング監視 | インスタンス数 | タスク数(Desired vs Running) | タスク数ギャップの監視追加 |
| ログ収集 | Agent設定 | タスク定義で設定 | タスク定義にログ設定追加 |
| ヘルスチェック | ALBのみ | ALB + コンテナヘルスチェック | タスク定義にヘルスチェック追加 |
| ディスク監視 | Agent必要 | Container Insights(オプション) | Fargateは一時ストレージのみ |
7.2 移行時の具体的な手順
1. Container Insightsの有効化
// ECSクラスタ作成時
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
containerInsights: true, // 必ず有効化
});
2. タスク定義でのログ設定
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
cpu: 256,
memoryLimitMiB: 512,
});
taskDefinition.addContainer('app', {
image: ecs.ContainerImage.fromRegistry('nginx'),
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'ecs',
logRetention: logs.RetentionDays.ONE_WEEK,
}),
// コンテナヘルスチェック
healthCheck: {
command: ['CMD-SHELL', 'curl -f http://localhost/ || exit 1'],
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
retries: 3,
startPeriod: cdk.Duration.seconds(60),
},
});
3. タスク定義のリソース設定
タスクのCPU/メモリ設定は、以下の組み合わせから選択します。
| CPU (vCPU) | メモリ (GB) | 用途 |
|---|---|---|
| 0.25 | 0.5, 1, 2 | 小規模API |
| 0.5 | 1, 2, 3, 4 | 軽量Webアプリ |
| 1 | 2, 3, 4, 5, 6, 7, 8 | 標準Webアプリ |
| 2 | 4〜16 | 中規模アプリ |
| 4 | 8〜30 | 大規模アプリ |
適切な設定方法:
- 最小構成から開始(例: 0.5 vCPU, 1GB)
- CloudWatchメトリクスで実際の使用率を確認
- CPU/メモリ使用率が80%を超える場合、タスク定義を更新
7.3 移行時の注意点
1. Container Insightsのコスト
注意: Container Insightsは追加コストが発生します。
- 有効化すると、カスタムメトリクスとして課金
- 1タスクあたり月額約$0.30〜$1.00
- 事前にコストを見積もる
推奨:
- 本番環境: 有効化(詳細な監視が重要)
- 開発環境: 基本メトリクスのみ(コスト削減)
2. タスク定義でのリソース設定
EC2との違い: Fargateでは、CPUとメモリの組み合わせが制限されています。
注意点:
- 過小設定: OOM Killerがタスクを強制終了(EC2のようなスワップなし)
- 過大設定: コストの無駄(使用していないリソースにも課金)
ベストプラクティス:
- 初期設定: 余裕を持たせる(例: CPU使用率が50%程度になるように)
- 継続的に見直し: 1〜2週間ごとにメトリクスを確認し、最適化
3. ヘルスチェックの設計
FargateではALBヘルスチェックとコンテナヘルスチェックの両方を設定できます。
ALBヘルスチェック:
- ALBからバックエンドへのHTTPリクエスト
- 失敗すると、ALBがトラフィックを振り分けない
コンテナヘルスチェック:
- タスク定義で設定
- 失敗すると、タスクが再起動される
設定の考え方:
- ALBヘルスチェック:
/healthエンドポイント(軽量) - コンテナヘルスチェック: より厳密なチェック(データベース接続確認など)
注意: 過度に厳しいヘルスチェックは、タスクの不要な再起動を引き起こします。
4. ログ収集の設計
EC2構成:
- CloudWatch Logs Agentのインストール・設定が必要
- ログローテーションの管理
Fargate構成:
- タスク定義でログドライバーを指定するだけ
- ログローテーションは自動
設定例:
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'ecs',
logRetention: logs.RetentionDays.ONE_WEEK, // 本番は30日推奨
})
コスト最適化
Fargate構成における監視コストの最適化テクニックを紹介します。
8.1 Container Insightsのコスト最適化
1. 環境ごとの使い分け
const isProduction = process.env.ENVIRONMENT === 'production';
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
containerInsights: isProduction, // 本番のみ有効化
});
2. 必要なメトリクスのみアラート設定
すべてのContainer Insightsメトリクスをアラーム対象にするのではなく、重要なもののみに絞ります。
推奨: 基本的には標準メトリクス(CPUUtilization、MemoryUtilization)で十分。Container Insightsは詳細調査用のダッシュボードとして活用。
3. コスト見積もり
例: 10タスクを常時稼働させる場合
- Container Insights: 約$3〜$10/月
- CloudWatchアラーム: 10個 × $0.10 = $1/月
- CloudWatch Logs: ログ量による(通常$1〜$5/月)
合計: 約$5〜$16/月
8.2 タスク定義の最適化
1. リソース使用率の継続的な確認
// ダッシュボードで使用率を可視化
new cloudwatch.GraphWidget({
title: 'ECS CPU vs Reserved',
left: [
new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'CpuUtilized',
dimensionsMap: { ServiceName: serviceName, ClusterName: clusterName },
statistic: 'Average',
label: 'Utilized',
}),
new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'CpuReserved',
dimensionsMap: { ServiceName: serviceName, ClusterName: clusterName },
statistic: 'Average',
label: 'Reserved',
}),
],
});
最適化の目安:
- CPU使用率が50%以下が継続: タスク定義を小さくする
- CPU使用率が80%以上が継続: タスク定義を大きくするか、スケールアウト
2. Auto Scalingの活用
トラフィックに応じてタスク数を動的に調整します。
const scalableTarget = service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scalableTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
8.3 ログの最適化
1. ログ保持期間の設定
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'ecs',
logRetention: logs.RetentionDays.ONE_WEEK, // 開発環境
// logRetention: logs.RetentionDays.ONE_MONTH, // 本番環境
})
2. ログレベルの調整
アプリケーションのログレベルを環境変数で制御します。
environment: {
LOG_LEVEL: 'INFO', // 本番環境
// LOG_LEVEL: 'DEBUG', // 開発環境のみ
}
まとめ
本記事では、ECS Fargateを採用したコンテナ構成における監視設計を、EC2構成との違いを明確にしながら解説しました。
重要なポイント
1. EC2構成との主な違い
| 観点 | EC2構成 | Fargate構成 |
|---|---|---|
| 監視単位 | インスタンス | タスク |
| メトリクス取得 | CloudWatch Agent必要 | Container Insights(標準) |
| 動的性 | 比較的静的 | タスクが頻繁に起動・停止 |
| ログ収集 | Agent設定 | タスク定義で簡単に設定 |
2. Fargate構成で特に重要なメトリクス
ECSタスク層:
- CPUUtilization(タスクレベル): タスク定義の適切性を判断
- MemoryUtilization(タスクレベル): OOM Killerによる強制終了を防ぐ
- RunningTaskCount: タスクの可用性を監視
- DesiredTaskCount vs RunningTaskCount: タスク起動失敗を検知
ALB層とRDS層:
- 基本的にEC2構成と同じ監視設計を適用可能
3. Container Insightsの活用
メリット:
- 標準で豊富なメトリクスを取得可能
- CloudWatch Agentのような設定不要
- 自動ダッシュボードで全体像を把握
注意点:
- 追加コストが発生(1タスクあたり月額約$0.30〜$1.00)
- 環境ごとに有効化/無効化を使い分ける
4. コンテナ監視の利点
- より細かい粒度: タスクレベルでの監視により、問題の特定が容易
- 標準化されたログ収集: タスク定義で簡単に設定
- 自動スケーリングとの連携: メトリクスベースの迅速なスケーリング
- インフラ管理の簡素化: CloudWatch Agentの管理が不要
次のステップ
監視設計は、一度設定したら終わりではありません。以下のサイクルで継続的に改善していきましょう:
- デプロイ: CDKで監視基盤を構築
- 観察: Container Insightsダッシュボードで傾向を確認
- 調整: タスク定義のリソース設定を最適化
- 改善: アラートの発生頻度を確認し、しきい値を調整
EC2構成との併用
実際の環境では、EC2とFargateを併用することもあります。
- Fargate: ステートレスなWebアプリケーション
- EC2: バッチ処理、GPUが必要な処理
この場合、本記事と前回記事の監視設計を組み合わせることで、統合的な監視基盤を構築できます。
関連記事:
参考リンク: