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?

何を監視すべきか迷わないために - ECS Fargateコンテナ監視設計パターン

13
Posted at

はじめに

前回の記事「何を監視すべきか迷わないために - 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つのメトリクスを使用します。

必須メトリクス

前回記事と同じメトリクスを監視します:

  1. TargetResponseTime(ターゲット応答時間) - Latency
  2. HTTPCode_Target_5XX_Count(5xxエラー数) - Errors
  3. HealthyHostCount(正常なホスト数) - Saturation + Errors
  4. 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タスクかの違いだけで、監視設計は変わりません。

必須メトリクス

前回記事と同じメトリクスを監視します:

  1. DatabaseConnections(データベース接続数) - Saturation
  2. CPUUtilization(CPU使用率) - Saturation
  3. FreeableMemory(空きメモリ) - Saturation
  4. ReadLatency / WriteLatency(読み取り/書き込みレイテンシ) - Latency
  5. 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 大規模アプリ

適切な設定方法:

  1. 最小構成から開始(例: 0.5 vCPU, 1GB)
  2. CloudWatchメトリクスで実際の使用率を確認
  3. 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の管理が不要

次のステップ

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

  1. デプロイ: CDKで監視基盤を構築
  2. 観察: Container Insightsダッシュボードで傾向を確認
  3. 調整: タスク定義のリソース設定を最適化
  4. 改善: アラートの発生頻度を確認し、しきい値を調整

EC2構成との併用

実際の環境では、EC2とFargateを併用することもあります。

  • Fargate: ステートレスなWebアプリケーション
  • EC2: バッチ処理、GPUが必要な処理

この場合、本記事と前回記事の監視設計を組み合わせることで、統合的な監視基盤を構築できます。


関連記事:

参考リンク:

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?