mackerel-container-agent が自動退役に失敗するので、Lambda 関数で確実に退役させる

![]() |
こんにちは、保坂です。 |
|---|
NTT東日本ではクラウド導入・運用サービスという、お客さまのクラウド移行・利用を支援するサービスを展開しております。
このサービスでは、お客さまのクラウドリソースの監視に株式会社はてな社の Mackerel というサービスを利用しています。
Mackerel(マカレル): 始めやすくて奥深い、可観測性プラットフォーム
Mackerel は非常に完成度が高く重宝しているサービスであるのですが、ECS / EKS などの監視を行う際に利用する mackerel-container-agent で登録されたホストが自動退役に失敗し、監視対象が既に存在しないにも関わらず Mackerel 上に残り続けてしまう…という事象に悩んでおりました。
このコラムでは Mackerel ホストの自動退役に失敗する原因やその再現と、Lambda 関数を利用して確実に自動退役させる仕組みの実装例をご紹介します。
NTT東日本では、AWSなどクラウドに関するお役立ち情報をメールマガジンにて発信していますので、ぜひこちらからご登録ください。
1. mackerel-container-agent を利用した監視と自動退役に失敗する状況
mackerel-container-agentは Amazon ECS や Kubernetes などを監視する際に利用し、以下の様な特徴を持っています。
- コンテナオーケストレーションプラットフォームを対象とした専用の監視エージェントです
- Dockerイメージで提供します
- タスク(ECS)やPod(Kubernetes)を監視します
- タスク/Podのサイドカーとして実行します
- タスク/Podをホストとして扱い、サービス、ロールを割り当てることが可能です
- エージェント終了時に自動退役します
この mackerel-container-agent を利用して監視しているタスク / Pod が終了する際、mackerel-container-agent は Mackerel に対して自動的に退役処理を実施し、Mackerel のホストとしても退役(Mackerel の監視対象外とすること)させる事ができます。
この時ネットワークの問題や mackerel-container-agent が退役処理を実施する前にタスク / Pod が停止されるなどによって、Mackerel のホストとして退役することができずタスク / Pod が存在しないにも関わらず残り続ける、という状況が発生いたします。
NTT東日本では、AWSなどクラウドに関するお役立ち情報をメールマガジンにて発信していますので、ぜひこちらからご登録ください。
2. 自動退役に失敗する事象の再現
2-1. 再現方法
まずは mackerel-container-agent を利用した監視において、自動退役に失敗する事象を再現してみます。
再現方法は以下としました。
- 2AZ にタスクをデプロイする ECS Cluster / Serivce を作成
- 片方の AZ の VPC にひもづく NACL において HTTPS 通信をブロック
- Service 設定で必要なタスク数を 2 → 0 としタスクを終了させる
2-2. 再現結果
結果は画像の通りで、ECS タスクは終了しているのにも関わらず NACL で退役処理をブロックしたホストは Mackerel 上に残ったままとなる状態を再現できました。
NTT東日本では、AWSなどクラウドに関するお役立ち情報をメールマガジンにて発信していますので、ぜひこちらからご登録ください。
3. Lambda関数を利用した確実に退役させる実装サンプル
Mackerel のドキュメントや「自動退役に失敗する事象の再現」からも分かるように、mackerel-container-agent 自身が退役処理を実施するのが本事象の根本的な原因となります。
そのため mackerel-container-agent 以外、今回は Lambda 関数から 退役処理を実施することで確実に Mackerel 上から該当のホストを退役させます。
具体的には以下となります。
- EventBridge ルールを使用し ECS からの ECS Task State Change 且つ lastStatus: STOPPED なイベントをトリガー
- Lambda 関数にて上記イベントから Mackerel 上に該当するホストを取得、そのホストに対し退役処理を実施
3-1. 実装サンプル
Lambda 関数は EventBridge より受信したイベントを元に、以下の処理を実施して退役処理を行う実装としました。
- EventBridge より受信したイベントから「taskId」を取得
-
Mackerel よりホスト名が「taskId」であるホスト情報を取得
- mackerel-container-agent によって正常に退役されていれば、ホスト情報の取得は出来ない
- Mackerel に対し取得したホストの退役処理を実施
実装サンプルは SAM として以下に記載しておりますので、参考にしていただければ幸いです。
template.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: "MackerelECSTaskHostRetirer"
Parameters:
TargetECSClusterArn:
Description: "The ARN of the target ECS Cluster"
Type: "String"
TargetECSServiceName:
Description: "The name of the target ECS Service"
Type: "String"
MackerelApiKey:
Description: "API key to the Mackerel organizaton"
NoEcho: true
Type: "String"
Resources:
MackerelApiKeySecret: # Mackerel API キー格納用 Secret
Type: "AWS::SecretsManager::Secret"
Properties:
Description: "Mackerel オーガニゼーションの API キー"
Name: "mackerel-organization-api-key"
SecretString: !Sub "{\"apiKey\": \"${MackerelApiKey}\"}"
MackerelECSTaskHostRetirer: # ECS タスク終了時に該当の Mackerel ホストを退役させる Lambda 関数のサンプル
Type: "AWS::Serverless::Function"
Properties:
Description: "ECS タスク終了時に該当の Mackerel ホストを退役させる Lambda 関数のサンプル"
Environment:
Variables:
MACKEREL_API_KEY_SECRET: !Ref "MackerelApiKeySecret"
Events:
ECSTaskStopped:
Type: "EventBridgeRule"
Properties:
Pattern:
source:
- "aws.ecs"
detail-type:
- "ECS Task State Change"
detail:
clusterArn:
- !Ref "TargetECSClusterArn"
lastStatus:
- "STOPPED"
group:
- !Sub "service:${TargetECSServiceName}"
FunctionName: "mackerel-ecs-task-host-retirer"
Handler: "lambda_function.lambda_handler"
MemorySize: 128
PackageType: "Zip"
Policies:
- "AWSLambdaBasicExecutionRole"
- Statement:
- Effect: "Allow"
Action: "secretsmanager:GetSecretValue"
Resource:
- !Ref "MackerelApiKeySecret"
Timeout: 10
Runtime: "python3.13"
lambda_function.py
import os
import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen
import boto3
class MackerelHostNotFoundError(Exception):
"""Mackerel上に指定したホストが存在しない場合の例外
Attributes
----------
host_name : str | None
存在しなかったホストのホスト名
host_id : str | None
存在しなかったホストのホストID
"""
def __init__(self, host_name: str | None = None, host_id: str | None = None) -> None:
self.host_name: str | None = host_name
self.host_id: str | None = host_id
if host_name:
message = f"Mackerel上にホスト名:{host_name}を持つホストが見つかりません"
elif host_id:
message = f"Mackerel上にホストID:{host_id}を持つホストが見つかりません"
else:
message = "Mackerel上に指定したホストが見つかりません"
super().__init__(message)
class MackerelHostNameDuplicateError(Exception):
"""Mackerel上に指定したホスト名を持つホストが複数ある場合の例外
Attributes
----------
host_name : str
同じホスト名を持つホストが複数あったホスト名
hosts : list[dict]
ホスト情報
"""
def __init__(self, host_name: str, hosts: list[dict]) -> None:
self.host_name: str = host_name
self.hosts: list[dict] = hosts
super().__init__(f"Mackerel上にホスト名:{host_name}を持つホストが{len(hosts)}ホスト存在します")
class ECSTaskHostRetirer:
"""ECSタスク毎ホストのタスク終了時に退役APIを実行するクラス
Attributes
----------
BASE_URL : str
Mackerel API のベース URL
"""
BASE_URL: str = "https://api.mackerelio.com"
def __init__(self, event: dict) -> None:
"""Initializer
Parameters
----------
event: dict
lambda_handlerに渡されるevent
"""
self._event: dict = event
self._mackerel_api_key: str = self._get_mackerel_api_key()
def _get_mackerel_api_key(self) -> str:
"""Secrets ManagerからMackerel APIキーを取得する
Returns
-------
str
取得したMackerel APIキー
"""
client = boto3.client("secretsmanager")
response: dict = client.get_secret_value(SecretId=os.environ["MACKEREL_API_KEY_SECRET"])
return json.loads(response["SecretString"])["apiKey"]
def _get_http_headers(self) -> dict[str, str]:
"""Mackerel APIリクエスト用のHTTPヘッダーを生成する
Returns
-------
dict[str, str]
APIキーとContent-Typeを含むHTTPヘッダー
"""
return {
"X-Api-Key": self._mackerel_api_key,
"Content-Type": "application/json"
}
def execute(self) -> None:
"""ECSタスクホスト退役処理のライフサイクルフック
Raises
------
MackerelHostNameDuplicateError
ホスト名が重複している場合
"""
task_name: str = self._get_task_name()
try:
host_id: str = self._get_host_id(task_name)
self._retire_host(host_id)
print(f"ECSタスク {task_name} を退役させました")
except MackerelHostNotFoundError:
print(f"ECSタスク {task_name} は 'mackerel-container-agent' により退役済みです")
def _get_task_name(self) -> str:
"""EventBridgeからのECSタスク終了イベントからタスクIDを取得する
Returns
-------
str
タスクID
"""
task_arn: str = self._event["resources"][0]
return task_arn.split("/")[-1]
def _get_host_id(self, host_name: str) -> str:
"""Mackerelから指定したホスト名のホストIDを取得する
Parameters
----------
host_name : str
ホスト名
Returns
-------
str
ホストID
Raises
------
MackerelHostNotFoundError
ホストが存在しない場合
MackerelHostNameDuplicateError
ホスト名が重複している場合
"""
url: str = f"{self.BASE_URL}/api/v0/hosts?name={host_name}"
req: Request = Request(url, headers=self._get_http_headers(), method="GET")
with urlopen(req) as res:
result: str = res.read().decode("utf-8")
hosts: list[dict] = json.loads(result)["hosts"]
if not hosts:
raise MackerelHostNotFoundError(host_name=host_name)
if len(hosts) > 1:
raise MackerelHostNameDuplicateError(host_name, hosts)
return hosts[0]["id"]
def _retire_host(self, host_id: str) -> None:
"""Mackerelから指定したホストIDのホストを退役させる
Parameters
----------
host_id : str
ホストID
Raises
------
MackerelHostNotFoundError
ホストIDが存在しない場合
HTTPError
その他のHTTPエラー
"""
url: str = f"{self.BASE_URL}/api/v0/hosts/{host_id}/retire"
data: bytes = json.dumps({}).encode("utf-8")
req: Request = Request(url, data=data, headers=self._get_http_headers(), method="POST")
try:
with urlopen(req) as res:
_ = res.read().decode("utf-8")
except HTTPError as e:
if e.code == 404:
raise MackerelHostNotFoundError(host_id=host_id)
raise e
def lambda_handler(event: dict, context: object) -> None:
"""AWS Lambdaハンドラー
Parameters
----------
event : dict
イベントデータ
context : object
Lambdaコンテキスト
"""
print(json.dumps(event))
ECSTaskHostRetirer(event).execute()
3-2. 動作確認
確実に退役させる Lambda 関数をデプロイしたら、「自動退役に失敗する事象の再現」と同じ要領で事象を再現させます。
以下から mackerel-container-agent による退役処理を NACL でブロックした場合も Mackerel 上から退役できている事が確認できます。
また Lambda 関数のログからも以下を確認することができました。
- NACL で退役処理をブロックしていない方のホストは mackerel-container-agent によって正常に退役できており、Lambda 関数実行時に該当のホストが Mackere 上に存在しない
- NACL で退役処理をブロックした方のホストは mackerel-container-agent によって退役できておらず、Lambda 関数によって退役させられた
NTT東日本では、AWSなどクラウドに関するお役立ち情報をメールマガジンにて発信していますので、ぜひこちらからご登録ください。
4. さいごに
本コラムでは mackerel-container-agent による自動退役が失敗する事象とその原因、対策の実装サンプルをご紹介しました。
タスク / Pod はオートスケーリングなどによってタスク数が変動するものだと思いますし、スケールアウト→スケールインを繰り返す内に本事象によって、存在しないタスクを監視する Mackerel ホストがいつの間にか発生していると余計なコストが発生してしまうことにもなります。
mackerel-container-agent 以外から退役処理を実施し、タスク / Pod 終了時に確実に退役させることでそういった余計なコストの発生も抑えることができます。
本コラムが誰かの参考になりましたら幸いです。
NTT東日本では経験豊かなエンジニアが、AWSの構築保守からネットワーク設計を含めエンドツーエンドでのソリューションを提供しております。ぜひお気軽にお問い合わせください。
- Amazon Web Services(AWS)および記載するすべてのAmazonのサービス名は、米国その他の諸国における、Amazon.com, Inc.またはその関連会社の商標です。
- Mackerelは、株式会社はてなの商標です。
RECOMMEND
その他のコラム
相談無料!プロが中立的にアドバイスいたします
クラウド・AWS・Azureでお困りの方はお気軽にご相談ください。







