前回書いた記事に続き,Mackerel を使って ECS のメトリクスを取得する方法を検証していて,今回は「パターン2 : コンテナインスタンスに mackerel-agent をインストールする」の検証結果をまとめる.前提としては前回と同じで,ALB の動的ポートマッピングを使う.
- パターン1 : mackerel-agent タスクをコンテナインスタンスごとに起動する
- パターン2 : コンテナインスタンスに mackerel-agent をインストールする
メリデメ
最初に「パターン2」のメリデメを整理しておく.「パターン1」では実現できなかった「タスク特有のメトリクスを取得できる」点が大きなメリットで,今回この「パターン2」を採用することにした.
- メリット
- mackerel-plugin-docker でコンテナインスタンスのメトリクスを取得できる
- mackerel-plugin-docker が Docker API からメトリクスを取得するため,動的ポートマッピングでタスクが増えた場合も,基本的なメトリクスは取得できる
- mackerel-plugin-gostats など,任意のプラグインを使って,動的ポートマッピングでタスクが増えた場合も,タスク特有のメトリクスを取得できる
- 少し工夫が必要になる(本記事のポイント)
- デメリット
- mackerel-agent のインストールは cloud-init で頑張る(逆に cloud-init でプロビジョニングができるのはメリットと言えるかも?)
- Mackerel のグラフ生成が難しい
構成図
「パターン2」の構成は以下のようになる.コンテナインスタンスに mackerel-agent をインストールしている点が「パターン1」とは異なる.
前提
今回は mackerel-plugin-gostats
を使って Golang のメトリクスを取得するため,前提として API に fukata/golang-stats-api-handler
を導入しておく必要がある.実装は割愛するが,エンドポイントとしては /stats
を用意した.
コンテナインスタンスの起動設定に指定する cloud-init 実装例
あくまで実装例として紹介するが,コンテナインスタンスに最低限必要になるプロビジョニングを全て cloud-init で実行した.こうすると Chef や Ansible を実行する必要もなく,Auto Scaling で負荷に応じて自動的に増減させることができる.AMI にする案もあるが,ECS で使う ECS-Optimized AMI も定期的に更新するだろうし,今はこのままにしている.
cloud-init に実装した内容をザックリと箇条書きにすると,以下のようになる.
timezone
- タイムゾーンを変更する
write_files
/etc/mackerel-agent/mackerel-agent.conf
を生成する- 動的ポートマッピングに対応したメトリクスを取得するための
/root/ecs_gostats.sh
を生成する
runcmd
mackerel-agent
とmackerel-agent-plugins
をインストールする/etc/ecs/ecs.config
にクラスタ名を書き込む
cloud-init の実装例としては,以下のようになる.ロール名 / コンテナ名 / クラスタ名などは,適宜読み替えてもらえればと!
#cloud-config timezone: "Asia/Tokyo" write_files: - path: /etc/mackerel-agent/mackerel-agent.conf permissions: 0644 content: | apikey = "xxx" roles = [ "yyy:zzz" ] include = "/etc/mackerel-agent/conf.d/*.conf" [host_status] on_start = "working" [plugin.metrics.gostats] command = "/root/ecs_gostats.sh" [plugin.metrics.docker] command = "/usr/bin/mackerel-plugin-docker" - path: /root/ecs_gostats.sh permissions: 0755 content: | #!/bin/sh if [ "${MACKEREL_AGENT_PLUGIN_META}" != "1" ];then for PORT in $(docker ps --format='{{.Ports}}' --filter name=api | cut -f1 -d- | cut -f2 -d:); do /usr/bin/mackerel-plugin-gostats -port ${PORT} -path=/stats -metric-key-prefix=gostats.${PORT} done else /usr/bin/mackerel-plugin-gostats -metric-key-prefix=gostats.# fi runcmd: - curl -fsSL https://mackerel.io/file/script/amznlinux/setup-yum.sh | sh - sudo yum install -y mackerel-agent - sudo yum install -y mackerel-agent-plugins - echo AUTO_RETIREMENT=1 >> /etc/sysconfig/mackerel-agent - mkdir /etc/mackerel-agent/conf.d/ - service mackerel-agent start - sh -c 'echo ECS_CLUSTER=api-cluster >> /etc/ecs/ecs.config'
cloud-init の詳細は以下に載っている.
Amazon Linux に mackerel-agent
をインストールする手順は以下に載っている.
mackerel-agent.conf
の詳細は以下に載っている.
ecs_gostats.sh
今回のポイントは,この ecs_gostats.sh
だと思う.
#!/bin/sh if [ "${MACKEREL_AGENT_PLUGIN_META}" != "1" ];then for PORT in $(docker ps --format='{{.Ports}}' --filter name=api | cut -f1 -d- | cut -f2 -d:); do /usr/bin/mackerel-plugin-gostats -port ${PORT} -path=/stats -metric-key-prefix=gostats.${PORT} done else /usr/bin/mackerel-plugin-gostats -metric-key-prefix=gostats.# fi
まず,動的ポートマッピングに対応するため docker ps
の結果から,ポートを抽出する.以下の例で言うと 32768
/ 32770
/ 32771
となる.
PORTS 0.0.0.0:32768->8080/tcp 0.0.0.0:32770->8080/tcp 0.0.0.0:32771->8080/tcp
次にそのポートごとに -metric-key-prefix
オプションを使って,メトリクスを送信する.具体的には -metric-key-prefix=gostats.${PORT}
とする.ポート番号 32768
を例にすると,実際取得できるメトリクスのキーは以下のようになる.
gostats.32768.gc.gc_num gostats.32768.gc.gc_pause_per_second gostats.32768.gc.gc_per_second gostats.32768.heap.heap_idle gostats.32768.heap.heap_inuse gostats.32768.heap.heap_released gostats.32768.heap.heap_sys gostats.32768.memory.memory_alloc gostats.32768.memory.memory_stack gostats.32768.memory.memory_sys gostats.32768.operation.memory_frees gostats.32768.operation.memory_lookups gostats.32768.operation.memory_mallocs gostats.32768.runtime.cgo_call_num gostats.32768.runtime.goroutine_num
さらに,重要なのは MACKEREL_AGENT_PLUGIN_META
と言う環境変数の存在で,実は今まで知らなかった.簡単に言うと MACKEREL_AGENT_PLUGIN_META=1
でプラグインを実行すると,グラフ定義を出力する仕様になっている.今回はメトリクス名にポート番号を含めているため,グラフ定義も変更する必要がある.具体的には -metric-key-prefix=gostats.#
と書いて,ポートの部分をワイルドカードで表現することができる.
すると,以下のようなグラフ定義を出力することができる.
gostats.#.gc
gostats.#.heap
gostats.#.memory
gostats.#.operation
gostats.#.runtime
JSON の部分は可読性のために整形しているが,実際はこんなグラフ定義を出力することができる.
$ MACKEREL_AGENT_PLUGIN_META=1 /usr/bin/mackerel-plugin-gostats -metric-key-prefix=gostats.# # mackerel-agent-plugin { "graphs": { "gostats.#.gc": { "label": "Gostats.# GC", "unit": "float", "metrics": [ { "name": "gc_num", "label": "GC Num", "stacked": false }, { "name": "gc_per_second", "label": "GC Per Second", "stacked": false }, { "name": "gc_pause_per_second", "label": "GC Pause Per Second", "stacked": false } ] }, "gostats.#.heap": { "label": "Gostats.# Heap", "unit": "bytes", "metrics": [ { "name": "heap_sys", "label": "Sys", "stacked": false }, { "name": "heap_idle", "label": "Idle", "stacked": false }, { "name": "heap_inuse", "label": "In Use", "stacked": false }, { "name": "heap_released", "label": "Released", "stacked": false } ] }, "gostats.#.memory": { "label": "Gostats.# Memory", "unit": "bytes", "metrics": [ { "name": "memory_alloc", "label": "Alloc", "stacked": false }, { "name": "memory_sys", "label": "Sys", "stacked": false }, { "name": "memory_stack", "label": "Stack In Use", "stacked": false } ] }, "gostats.#.operation": { "label": "Gostats.# Operation", "unit": "integer", "metrics": [ { "name": "memory_lookups", "label": "Pointer Lookups", "stacked": false }, { "name": "memory_mallocs", "label": "Mallocs", "stacked": false }, { "name": "memory_frees", "label": "Frees", "stacked": false } ] }, "gostats.#.runtime": { "label": "Gostats.# Runtime", "unit": "integer", "metrics": [ { "name": "goroutine_num", "label": "Goroutine Num", "stacked": false }, { "name": "cgo_call_num", "label": "CGO Call Num", "stacked": false } ] } } }
ワイルドカードの仕様はよく理解できていなくて,難しいなと感じた.#
と *
の違いはなんだろう?とか,個数制限があるのは #
だけ?とか.ドキュメントとしては以下に載っている.
ホストグラフ : カスタムメトリック(成功 ○)
この時点で,既に動的ポートマッピングに対応したメトリクスが取得できているため,まずは「ホストグラフ : カスタムメトリック」を確認する.以下のように,ちゃんとポート別にメトリクスが確認できているし,タスク数の増加にも対応できている.ただし,これはまだコンテナインスタンスごとのメトリクスなので,実際にはあまり使わなそう.
ロールグラフ : カスタムメトリック(失敗 ×)
ロールグラフは完全に厳しくて,ワイルドカード指定で表示することができなかった.改善要望出したいなぁ.
ロールグラフ : カスタマイズグラフ(微妙 △)
こうなったらもうカスタマイズグラフで頑張るしかなさそうだ!ということになり,カッとなって作った.role
関数を使って,ポートの部分を *
で表現したらうまくグラフ化できた.けど,シンプルとは言えないし,ツライ.
https://mackerel.io/embed/orgs/xxx/advanced-graph?query=role('yyy:zzz', 'custom.gostats.*.memory.memory_sys')&title=custom.gostats.*.memory.memory_sys&period=1h https://mackerel.io/embed/orgs/xxx/advanced-graph?query=role('yyy:zzz', 'custom.gostats.*.memory.memory_sys')&title=custom.gostats.*.memory.memory_sys&period=1d https://mackerel.io/embed/orgs/xxx/advanced-graph?query=role('yyy:zzz', 'custom.gostats.*.memory.memory_sys')&title=custom.gostats.*.memory.memory_sys&period=1w
カスタマイズグラフを大量に作ってダッシュボードを用意した.
カスタマイズグラフに関しては以下に載っている.
去年のアドベントカレンダーでカスタマイズグラフで試行錯誤したのを思い出してしまった.
まとめ
今回実現した方法が正しいのかよくわからないし,もっと良い方法があるのかもしれないし,Mackerel が正式に ECS をサポートしてくれるかもしれないけど,現状この「パターン2」で考えていたことを実現することができた.とは言え,まだ検証環境で ECS を動かしている段階で,実環境に投入したらまた別の問題に気付くかもしれない.もっと良い方法があれば教えて欲しい.
- コンテナインスタンスに mackerel-agent をインストールした
- コンテナインスタンスのメトリクスを取得できた
- タスクのポートをワイルドカードで表現することで,タスクのメトリクスも取得できるようになった
- グラフは一筋縄では実現できず,カスタマイズグラフで頑張った
謝辞
困ったときに相談するとすぐに助けてくれる id:okzk に今回はアイデアをたくさん頂いた!感謝!