kmuto’s blog

はてな社でMackerel CREをやっています。料理と旅行といろんなIT技術

Mackerelのエージェントの設定ファイルの取得と更新反映を、ホストメタデータ領域を使って実現してみた

Mackerelにホストのリソースメトリックやミドルウェアのチェック結果を送るエージェント「mackerel-agent」について、API経由で設定の内容を外部から参照したり、あるいは外部から設定を送って適用させたりする仕組みを思いついたので、実装してみた。

オブザーバビリティプラットフォームのMackerelにおいて、各ホストのリソースメトリックやミドルウェアのチェック監視結果などはmackerel-agentという常駐エージェントを経由して送信されている(OpenTelemetryのメトリックやトレースの送信にはmackerel-agentではなくOpenTelemetry Collectorなどの手法を使うが、ここでは説明を割愛する)。

mackerel-agentは(基本的に)送信のみを行う設計思想で、MackerelのWebコンソールからmackerel-agentを制御するような仕組みは用意されていない。これは安全ではあるが、複数のホストを運用する場面では困ることもある。

  • あるホストのチェック監視の設定を見たいが、そのホストにログインしないとわからない
  • 設定を一括で更新したいが、何らかの構成管理(Ansibleなど)を使わない限り、1つひとつログインして設定を更新して回らなければならない

これに対し、Mackerelが以前から提供している……があまり知られているとは言えない、ホストメタデータ領域を使って対策してみよう。

Linuxを想定しているが、WindowsでもPowerShellなどを使えば類似のことを実現できるだろう。

ホストメタデータとは

ホストメタデータは、Mackerel上の各ホスト(スタンダードホスト、マイクロホスト)に格納可能な任意のJSON形式データである。ヘルプでは「運用上の管理データやパッケージのバージョンなど、様々な情報を登録してご利用いただけます。」という用法が案内されている。

mackerel.io

保存可能なサイズはホストあたり100KBまでに制限されているが、JSONで表現するオーバーヘッドを含めても、100KBにもなるようなmackerel-agent.confはあまりないだろう(そのくらいのサイズになっているのであれば、普通に構成管理ツールを使うことをお勧めする)。

mackerel.io

ホストメタデータにmackerel-agent.confの内容を記録する

リクエストできるのはAPIキーを知っているユーザーのみとはいえ、apikeyまで記録するのは少々ためらわれる。いったん抜いて記録するようにしよう。エスケープも含めたJSON化はjqコマンドに任せている。

grep -v '^apikey' /etc/mackerel-agent/mackerel-agent.conf | jq -sR '{cf: .}'
{
  "cf": "# pidfile = \"/var/run/mackerel-agent.pid\"\n# root = \"/var/lib/mackerel-agent\"\n# verbose = false\n\n# [host_status]\n# on_start = \"working\"\n# on_stop  = \"poweroff\"\n...

mackerel-agentがこれをホストメタデータとして1時間ごとに投稿するように、/etc/mackerel-agent/mackerel-agent.confで設定する。

...

[plugin.metadata.cf]
command = "grep -v '^apikey' /etc/mackerel-agent/mackerel-agent.conf | jq -sR '{cf: .}'"
execution_interval = "1h"

systemctl restart mackerel-agentでmackerel-agentを再起動すると、初回のメタデータ記録が実行されるはずだ。

apikeyだけでなく、環境変数あるいはパラメータ経由で認証して何か取得するようなプラグインもあるときには、別途PythonやRubyなどの高級なスクリプトを使い、マスキングなり除去なりをしたほうがよいだろう。

ホストメタデータからmackerel-agent.confの内容を取得する

内容の取得はホストメタデータの取得APIを使えばよい。JSONからファイル内容部を抽出するにはjqコマンドを使う。

curl -s https://api.mackerelio.com/api/v0/hosts/《ホストID》/metadata/cf -H 'X-Api-Key: 《APIキー》' | jq -r '.cf'
# pidfile = "/var/run/mackerel-agent.pid"
# root = "/var/lib/mackerel-agent"
# verbose = false
# apikey = ""

# [host_status]
# on_start = "working"
# on_stop  = "poweroff"
...

これで、ホストのmackerel-agent.confの記録と、(APIキーを知っていれば)どこでもその取得ができるようになった。

ホストメタデータの内容でmackerel-agent.confを更新する

続いては、「外部からホストメタデータにmackerel-agent.confの内容を置いて、それをホストに読ませて反映する」という、中央制御っぽい仕組みを実現してみよう。

考え方としては次のようになる。

  • ホスト側では、以下を実行するプラグインをmackerel-agentで動かす(プラグインではなくcronでもよい)
    1. ホストメタデータ領域に新しい内容のmackerel-agent.confが存在するか確認する。存在する場合、
    2. /etc/mackerel-agent/mackerel-agent.confをその内容で上書きし、
    3. ホストメタデータ領域からmackerel-agent.confのデータを削除して、
    4. 新しい内容を反映するためにmackerel-agentを再起動する
  • 中央制御する側では、ホストメタデータ領域に新しい内容のmackerel-agent.confcurlコマンドなどで書き込む(前述のmackerel-agent.confの記録もしている場合、2つのメタデータが格納されるタイミングが発生するので、サイズに注意する必要がある)

ホストメタデータ上で記録用のキーはcfとしていたが、今回の更新用のキーはucと名前を分けている。

ではプラグインを作成する。とはいえ、この程度だとシェルスクリプトでも簡単に書ける範囲だろう。チェック監視のプラグインとして、標準出力で出力したものが、ホスト詳細画面のチェック監視ステータスに出る。

#!/bin/bash
set -e

CONFPATH="/etc/mackerel-agent/mackerel-agent.conf"
HOSTID=$(cat /var/lib/mackerel-agent/id)
APIKEY=$(grep "^apikey" "$CONFPATH" | cut -d'"' -f2)

METALIST=$(curl -s "https://api.mackerelio.com/api/v0/hosts/$HOSTID/metadata" -H "X-Api-Key: $APIKEY")
RET=$(echo "$METALIST" | jq -r '.metadata | any(.namespace == "uc")')
if [ "$RET" != "true" ]; then
  echo "up to date (or no metadata)"
  exit 0
fi

CF=$(curl -s "https://api.mackerelio.com/api/v0/hosts/$HOSTID/metadata/uc" -H "X-Api-Key: $APIKEY" | jq -r '.cf // empty')

if [ -z "$CF" ]; then
  echo "failed to fetch metadata"
  exit 3
fi

NEWCONFPATH=$(mktemp "${CONFPATH}.XXXXXX")
trap 'rm -f "$NEWCONFPATH"' EXIT

echo "$CF" > "$NEWCONFPATH"

set +e
RET=$(mackerel-agent configtest -conf "$NEWCONFPATH" 2>&1)
EXITCODE=$?
set -e
if [ $EXITCODE -ne 0 ]; then
  echo "$RET"
  exit 3
fi

mv "$NEWCONFPATH" "$CONFPATH"
curl -s -X DELETE "https://api.mackerelio.com/api/v0/hosts/$HOSTID/metadata/uc" -H "X-Api-Key: $APIKEY" -H 'Content-Type: application/json' > /dev/null
systemctl restart mackerel-agent
echo "up to date"

内容のポイントとしては以下のとおりだ。

  • ホストのmackerel-agent.confにあるAPIキー、およびidにあるホストIDを使って、自身のホストメタデータucの取得を試す
  • 存在したら一時ファイルに書き出し、mackerel-agent configtestで最低限の構文確認を行う
  • 構文に問題がなければ/etc/mackerel-agent/mackerel-agent.confを置き換える
  • 役目を終えたホストメタデータucを削除する
  • mackerel-agentを再起動する
  • 各処理の途中でリクエスト失敗などの異常が生じたときは終了コード「3」を返し、Unknownアラートを発生させる(1はWarning、2はCriticalだが、このスクリプトではユーザー側に何かアクションできることはあまりない)

このスクリプト/usr/local/bin/check-update-conf.shに実行権限(chmod 755 /usr/local/bin/check-update-conf.sh)を付け、/etc/mackerel-agent/mackerel-agent.confに登録する。

...

[plugin.checks.update-conf]
command = ["/usr/local/bin/check-update-conf.sh"]
check_interval = 5

ホストメタデータの問い合わせはやや重めなリクエストなので、念のためにcheck_intervalで実行間隔を5分ごとに設定した。

systemctl restart mackerel-agentで再起動しておこう。

一方、中央制御している側から手元のmackerel-agent.conf-《ホストID》apikey行も含む完全な設定ファイル)をホストメタデータに送るには、curlを使って以下のように実行する(だいぶ長いワンライナーになってしまったので、スクリプト化してもよいだろう)。

cat mackerel-agent.conf-《ホストID》 | jq -sR '{cf: .}' | curl -X PUT "https://api.mackerelio.com/api/v0/hosts/《ホストID》/metadata/uc" -H 'X-Api-Key: 《APIキー》' -H 'Content-Type: application/json' -d @-

ここでは一時的とはいえapikeyも含めたすべてをメタデータに送ることを想定しているが、mackerel-agent.confではincludeで別ファイルから読ませることもできるので、次のような構成にすると、万が一中央制御のファイルやメタデータの内容が漏洩したときにも堅牢になる。

  • mackerel-agentのあるホストでは/etc/mackerel-agent/key.confapikey = "《APIキー》"の内容を入れておく。chmod 600 /etc/mackerel-agent/key.confを実行して、root権限でのみ読めるようにしておくのもよいだろう
  • mackerel-agent.confで最初にこのファイルを読むよう、冒頭にinclude = "/etc/mackerel-agent/key.conf"を書く

mackerel.io

まとめ

ホストメタデータの領域を使うことで、本来ホストにログインしないとわからなかったmackerel-agentの設定ファイルを参照したり、書き換えたりといった芸当ができるようになった。

工夫・発展しやすいテクニックだと思うので、セキュリティに注意しつつ、遊んでみてほしい。

たとえば……

  • ホストの情報を集めたカスタムダッシュボードのMarkdownウィジェットに貼り付け、構成を把握できるようにしてみる
  • 更新がわかるようにホストメモに更新日時を入れる
  • Tomlの設定ファイルをJSON化し(Pythonなどで簡単にできる)、必要な項目を取り出して管理下ホスト群の状況が一目でわかるようにしてみる
  • 大規模環境で集中管理を実現してみる