Technical Blog テクニカルブログ
  1. HOME
  2. テクニカルブログ
  3. AWS ECS(Fargate)のコンテナメモリ使用量を正確に取得する方法について

AWS ECS(Fargate)のコンテナメモリ使用量を正確に取得する方法について

投稿者:NI+C 四至本

目次

1. はじめ

こんにちは、NI+C AWSチームの四至本です。

私の担当しているプロジェクトではAWSとPythonを用いて処理モデルを開発しており、ECSのfargateを用いてコンテナを起動しスコアリング処理を行なっています。ECSでコンテナを運用しているとき、Container Insightsを有効化しCloudWatchを用いてメトリクスを監視することは多いと思いますが、ある処理モデルを実行した際に、
ECS上でタスクメモリ設定値を16GBで設定してるにも関わらず、CloudWatch上でメモリ使用量のメトリクスを確認すると4GBでエラー(Out Of Memory Error)」が発生しました。

実際にはメモリをより多く使用できるはずなのに、なぜ4GBを超えた時点でエラーになってしまったのか?
調査していくと、Container Insightsが提示するメトリクスとECSタスクメタデータで取得したメトリクスに違いがあることが分かりました。

今回は、私が直面した問題がなぜ起こったのか、どう対処したのかについて
・Container InsightsとECSタスクメタデータの違い
・ECSタスクメタデータで正確にメモリ使用量を取得する方法

のご紹介を交え説明いたします。

2. Container InsightsとECSタスクメタデータの違い

Container InsightsとECSタスクメタデータの違いについてご紹介します。

  • Container Insights

 ・コンテナ化されたアプリケーションとマイクロサービスのメトリクスとログを収集、集計、要約できるもの。
  ECSの設定時にContainer Insightsを有効化し、ECSのパフォーマンス監視を強化できる。
 ・メモリ使用量のメトリクス:MemoryUtilized

  • ECSタスクメタデータ(ECS_CONTAINER_METADATA_URI_V4)

 ・上記のエンドポイントを用いて、ECSで実行中のタスクやその中のコンテナに関する情報(メタデータ)を取得できるもの。
  ECSでは、このエンドポイントに関する情報が環境変数として設定されている。 
 ・メモリ使用量のメトリクス例:MemoryUsage

【収集できる統計情報の違い】

 ・Container Insightsのメトリクスは集計された統計情報であり、取得元や集計方法などの詳細は開示されていない。
 ・ECSのタスクメタデータから取得されたメトリクスはコンテナ内でリアルタイムに確認されたDocker統計情報。

上記2点より、取得・集計ロジックが異なることからECS上でタスクメモリ設定値を16GBで設定しているにも関わらず、CloudWatch上でメモリ使用量のメトリクスを確認すると4GBでエラー(Out Of Memory Error)が発生したと考えられます。
以上より、ECSタスクメタデータから取得される数値が実際のコンテナメモリ使用量であることが分かりました。

3. ECSタスクメタデータで正確にコンテナメモリ使用量を取得する方法

ここからは、正確なコンテナメモリ使用量を取得する方法について説明いたします。
結論としては、「ECSタスクメタデータのエンドポイント(${ECS_CONTAINER_METADATA_URI_V4}/task/stats)から取得したDocker統計情報を、PythonスクリプトでCloudWatchメトリクスとして送信」することで、正確なコンテナメモリ使用量を確認することができます。

今回紹介するソースコードを、必要なIAM権限を設定した上でメモリ使用量を確認したい対象ファイルと同じフォルダに格納・対象ファイルに少し必要なPythonスクリプトを追記するだけで、CloudWatch上でコンテナメモリ使用量を扱えるようになります。

以下では、この方法の仕組みを理解するために必要となるエンドポイントの概要や、Docker統計情報の詳細、そして実際に取得へ至るまでの手順を解説いたします。


  • エンドポイントの概要

${ECS_CONTAINER_METADATA_URI_V4}
 このパスはコンテナのメタデータを返す。

${ECS_CONTAINER_METADATA_URI_V4}/task
 このパスはタスクのメタデータを返す。

${ECS_CONTAINER_METADATA_URI_V4}/stats
 このパスは Docker コンテナの Docker 統計を返す。

${ECS_CONTAINER_METADATA_URI_V4}/task/stats
 このパスはタスクに関連付けられたすべてのコンテナの Docker 統計を返す。

「Fargate のタスク用の Amazon ECS タスクメタデータエンドポイントバージョン 4」
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-fargate.html


  • ${ECS_CONTAINER_METADATA_URI_V4}/task/statsから取得したDocker統計情報の詳細

エンドポイントから取得したDocker統計情報はJSON形式になります。
ここでは、必要なメモリ情報のみ説明いたします。
※下記のJSON形式は一部省略

"memory_stats": {
      "usage": 2023424,
      "max_usage": 5185536,
      "stats": {
        "active_anon": 0,
        "active_file": 0,
        "cache": 0,
        "dirty": 0,
        "hierarchical_memory_limit": 17179869184,
        "hierarchical_memsw_limit": 9223372036854771712,
        "inactive_anon": 1486848,
        "inactive_file": 0,
        "mapped_file": 0,
        "pgfault": 2125,
        "pgmajfault": 0,
        "pgpgin": 1623,
        "pgpgout": 1335,
        "rss": 1486848,
        "rss_huge": 0,
        "total_active_anon": 0,
        "total_active_file": 0,
        "total_cache": 0,
        "total_dirty": 0,
        "total_inactive_anon": 1486848,
        "total_inactive_file": 0,
        "total_mapped_file": 0,
        "total_pgfault": 2645,
        "total_pgmajfault": 0,
        "total_pgpgin": 1623,
        "total_pgpgout": 1345,
        "total_rss": 1486848,
        "total_rss_huge": 0,
        "total_unevictable": 0,
        "total_writeback": 0,
        "unevictable": 0,
        "writeback": 0
      },
      "limit": 9223372036854771712
    },
    "name": "process-container",

usage:コンテナが現在使用中のメモリ量

max_usage:コンテナが起動してから現在までに使用した最大のメモリ

hierarchical_memory_limit:メモリ量の制限値(ECSで設定しているタスクメモリ量)

その1:IAMロールにCloudWatchのPutMetricData API実行権限があるポリシーを追加

PutMetricData APIを使用し、エンドポイントから取得したDocker統計情報をCloudWatchにプッシュします。

IAMロール:ECSのロール

IAMポリシー:CloudWatchAgentAdminPolicy

その2:メモリ使用量を確認したいファイルが格納されているフォルダに下記のソースコードを格納

ここでは以下のようなフォルダ構成を例に、説明いたします。
(例)
Process/
├── container_memory_utilization.py
├── process1.py
├── process2.py
├── process3.py
└── main.py


コンテナメモリ使用量取得ソースコード(container_memory_utilization.py

"""ECSタスクメタデータからDocker統計情報を取得後、必要メモリ情報をCloudWatchメトリクスとして送信するモジュール
!! メトリクス名 !!
usage -> MemoryUsage
max_usage -> MaxMemoryUsage
hierarchical_memory_limit -> MemoryLimit
"""

import os
import time
import requests
import boto3


def fetch_metadata(url):
    try:
        # 統計情報エンドポイントに対してHTTP GETリクエストを送信
        response = requests.get(url)
        # ステータスコードが200以外の場合に例外を発生
        response.raise_for_status()
        # ステータスコードとレスポンス内容を表示
        print(f'status_code: {response.status_code}, content: {response.text}')
        # JSONデータを読み込み
        return response.json()
    except requests.RequestException as e:
        print(f'Error: {e}')


def calculate_memory_usage():
    print("*** メモリ使用量の確認 START ***")
    # ECSコンテナのメタデータURIを環境変数から取得
    metadata_uri = os.environ.get('ECS_CONTAINER_METADATA_URI_V4')
    if not metadata_uri:
        print("ECS_CONTAINER_METADATA_IRI_V4 is not set")

    # 統計情報更新のため遅延
    time.sleep(10)

    # メタデータの統計情報を取得するためのURLを構築
    url = f"{metadata_uri}/task/stats"
    task_stats = fetch_metadata(url)

    # メモリ使用量とコンテナ名を取得
    for key, value in task_stats.items():
        memory_usage = value.get("memory_stats", {}).get("usage")
        max_memory_usage = value.get("memory_stats", {}).get("max_usage")
        memory_limit = value.get("memory_stats", {}).get("stats", {}).get("hierarchical_memory_limit")
        container_name = value.get("name")
    if memory_usage is None:
        print("memory usage is not available in task stats")
    elif max_memory_usage is None:
        print("max memory usage is not available in task stats")
    elif memory_limit is None:
        print("hierarchical memory limit is not available in task stats")
    elif container_name is None:
        print("container name is not available in task stats")

    # B(bytes)をMBへ変換
    memory_usage_mb = memory_usage / (1024 * 1024)
    max_memory_usage_mb = max_memory_usage / (1024 * 1024)
    memory_limit_mb = memory_limit / (1024 * 1024)

    # AWSセッションを初期化し、CloudWatchクライアントを作成
    sessin = boto3.Session()
    cloudwatch = sessin.client("cloudwatch")

    # CloudWatchにメモリ使用量のメトリクスを送信
    try:
        response = cloudwatch.put_metric_data(
            Namespace="ECS/Container",
            MetricData=[
                {
                    "MetricName": "MemoryUsage",
                    "Dimensions": [
                        {
                            "Name": "ContainerName",
                            "Value": container_name
                        }
                    ],
                    "Value": memory_usage_mb,
                    "Unit": "Megabytes"
                },
                {
                    "MetricName": "MaxMemoryUsage",
                    "Dimensions": [
                        {
                            "Name": "ContainerName",
                            "Value": container_name
                        }
                    ],
                    "Value": max_memory_usage_mb,
                    "Unit": "Megabytes"
                },
                {
                    "MetricName": "MemoryLimit",
                    "Dimensions": [
                        {
                            "Name": "ContainerName",
                            "Value": container_name
                        }
                    ],
                    "Value": memory_limit_mb,
                    "Unit": "Megabytes"
                },
            ]
        )
        print("Successfully put metric data to CloudWatch")
        print("*** メモリ使用量の確認 END ***")
    except Exception as e:
        print(f'Failed to put metric data: {e}')

● ソースコードのポイント

・処理時間が短い場合、sleep()を用いて適度に遅延させる。
 メタデータサーバが統計情報を更新する頻度が低いため、連続してリクエストを送信した場合、
 同じデータ(リクエストに対してキャッシュされたデータ)が返されることがあるため。
・B(bytes)をMBへ変換は、ECSメトリクス「MemoryUtilized」の単位に合わしてるため任意に変更可能。
・ソースコード内で設定しているメトリクス名は、任意に変更可能。

その3:対象のファイルにcontainer_memoroy_utilization.pyの関数呼び出しコードを追記

例のような処理構成の場合、実行前、各処理完了時に関数(calculate_memory_usage())を記載すると詳細に確認できます。

(例) 「main.py」
from container_memory_utilization import calculate_memory_usage
print(“START”)
calculate_memory_usage()
Process1.run()
calculate_memory_usage()
Process2.run()

calculate_memory_usage()
Process3.run()
calculate_memory_usage()
print(“END”)

手順を実行するとCloudWatchの「メトリクス」にて確認可能

上記の手順を実施していくと下記のような各処理ごとにメモリ使用量のメトリクスが取得され、
CloudWatchの「メトリクス」にて確認することができます。
※メトリクス:MemoryUsage

また、取得したメトリクスからCloudWatchでアラーム設定も可能です。
CloudWatchの設定画面から手動で構築する方法と、CloudFormationを使って自動的に構築する方法があります。

4. 最後に

本記事でご紹介しましたように、Container InsightsとECSタスクメタデータではメトリクスの取得・集計ロジックが異なるため、表示されるメモリ使用量に差が生じることがあります。今回のケースでは、Container Insightsのメトリクスでは4GB付近でエラーのように見えていましたが、実際に上記の取得方法を用いて最大コンテナメモリ使用量(MaxMemoryUsage)を確認するとで16GB近くのメモリをしっかりと利用されていることが分かりました

ECSでコンテナを運用中、メモリ使用量にまつわる謎のエラーが発生したら、まずはECSタスクメタデータ(ECS_CONTAINER_METADATA_URI_V4)を利用して正確な値を取得してみることをおすすめします。
以上、ECSのコンテナメモリ使用量を正確に取得する方法のご紹介でした。読んでいただきありがとうございました!

補足ですが、2024年12月に「Amazon CloudWatch Container Insights が Amazon ECS のオブザーバビリティを強化」: Container Insightsはクラスターレベルからコンテナレベルまで詳細なメトリクスを確認できるようになったと発表がありました。
こちらについて、ご紹介した取得方法と比較し別の機会にご紹介できればと思います。

ページのトップへ