はじめに

監視まわりの exporter が増えてくると、単に動かすだけではなく、どれを rootful に置き、どれを rootless に置くかを揃えないと運用が崩れやすくなる。今回はその整理をしながら、smartctl-exporter を実際のパス構成に合わせて再現性のある形に落とし込んだ。

背景にあるのは、すでに進めていた node_exporter の全台展開だった。Prometheus 側で複数ノードを scrape する流れ自体は先にできていたので、次はデバイス寄りの exporter を安全に足せる運用標準が必要になった。特に smartctl-exporternvme0 側の癖や rootful / rootless の責務の混在で崩れやすかったので、ここを先に固めることにした。

背景と動機

node_exporter を各サーバーへ広げたメモでは、Storage サーバーの Prometheus に :9100 を順番に追加していくやり方を取っていた。あの時点で scrape 側の集約パターンはできていたが、exporter 自体の置き場所はサービスごとに揺れていた。

今回困っていたのは 2 点で、1 つは rootful / rootless をまたぐ依存が分かりにくくなること、もう 1 つは unit や compose を触ったあとに daemon-reload を忘れて、設定変更が反映されたつもりで進んでしまうことだった。加えて smartctl-exporter v0.14.0 は --smartctl.device-opts を使えないので、NVMe 周りの扱いを別途吸収しないと安定しにくい。

そのため、今回は個別の対処だけではなく、「exporter はどう置くべきか」「依存はどこまで systemd に持たせるか」「変更後に何を必ず叩くか」まで含めてひとつの運用メモにまとめた。

ディレクトリ標準

最初に、rootful / rootless の両方で迷わないようにディレクトリ標準を切った。ここが曖昧だと、unit の置き場所と compose の置き場所が都度ぶれて、サービスが増えるほど追いにくくなる。

  # ルートフル(管理者常駐)
sudo mkdir -p /opt/containers/{compose,systemd}/{rootful,rootless}
sudo mkdir -p /opt/prometheus/exporters/smartctl/bin
  

ポイントは、管理者常駐のものとユーザー常駐のものを同じ階層思想で並べることだった。smartctl-exporter のようにデバイスへ触るものは rootful 側、prometheusloki のようなアプリ層は rootless 側、と最初から寄せておくと後で見返しやすい。

systemd(rootful)

smartctl-exporter は rootful 側で oneshot + RemainAfterExit=yes のパターンに揃えた。ここはすでに動かしていた node-exporterpromtail などの作法と整合させる意図がある。

  # /etc/systemd/system/smartctl-exporter.service
[Unit]
Description=smartctl-exporter (rootful)
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/containers/compose/rootful
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
TimeoutStopSec=30s

[Install]
WantedBy=multi-user.target
  

有効化は標準的な flow で十分だった。

  sudo systemctl daemon-reload
sudo systemctl enable --now smartctl-exporter.service
  

この形にした理由は、ホスト依存の強い exporter は system scope で管理した方が挙動が素直だからだ。特にデバイスや権限が絡むものは rootless で無理をすると、最初は動いても保守のたびに説明コストが増える。

Compose(rootful / smartctl-exporter v0.14.0)

今回の主題はここだった。smartctl-exporter v0.14.0 では --smartctl.device-opts が使えないので、NVMe 向けの -T permissive をラッパーで付与して吸収した。

まず wrapper を用意する。

  sudo tee /opt/prometheus/exporters/smartctl/bin/smartctl-wrapper >/dev/null <<'SH'
#!/bin/sh
case " $* " in
  *"/dev/nvme"*) exec /usr/sbin/smartctl -T permissive -d nvme "$@";;
  *)             exec /usr/sbin/smartctl -T permissive "$@";;
esac
SH
sudo chmod +x /opt/prometheus/exporters/smartctl/bin/smartctl-wrapper
  

これで nvme0nvme1 に対して smartctl を叩く時も、まず permissive 側へ寄せられる。Apple NVMe 周辺の exit=4 を吸収したいという意図が明確なので、compose の引数だけで頑張るより分かりやすい。

compose 側は以下の形にした。

  # /opt/containers/compose/rootful/smartctl-exporter.yml
services:
  smartctl-exporter:
    image: docker.io/prometheuscommunity/smartctl-exporter:v0.14.0
    container_name: smartctl-exporter
    user: root
    privileged: true
    restart: unless-stopped
    ports:
      - "9633:9633"
    devices:
      - /dev/nvme0:/dev/nvme0
      - /dev/nvme1:/dev/nvme1
    volumes:
      - /opt/prometheus/exporters/smartctl/bin/smartctl-wrapper:/usr/local/bin/smartctl-wrapper:ro
    command:
      - --web.listen-address=:9633
      - --smartctl.path=/usr/local/bin/smartctl-wrapper
      - --smartctl.interval=60s
      - --smartctl.timeout=3s
      - --smartctl.retries=0
      - --smartctl.device=/dev/nvme0
      - --smartctl.device=/dev/nvme1
  

ここで大事なのは --smartctl.timeout=3s--smartctl.retries=0 を明示していることだった。/metrics が固まると Prometheus 側で見える問題よりも前に exporter 自体の疎通が崩れるので、まずは短く諦める設定に振った方が運用しやすい。

起動と確認も最短で済ませる。

  sudo systemctl restart smartctl-exporter.service
curl -s 127.0.0.1:9633/metrics | grep 'smartctl_device{device="nvme'
  

もしここでまだ固まる場合は、いきなり全デバイスを追わずに nvme1 だけへ絞る。

  # 一時的にnvme1のみ
command:
  - --web.listen-address=:9633
  - --smartctl.path=/usr/local/bin/smartctl-wrapper
  - --smartctl.device=/dev/nvme1
  - --smartctl.interval=60s
  - --smartctl.timeout=3s
  - --smartctl.retries=0
  

これは逃げではなく切り分けとして正しい。監視系はまず scrape できる状態へ戻してから、問題デバイスを戻す方が全体の見通しがよくなる。

systemd(rootless)の雛形

rootless 側も unit の型を揃えておく。例として mktxp.service の雛形を置いた。

  # /opt/containers/systemd/rootless/mktxp.service
[Unit]
Description=Podman Compose Stack for mktxp
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/prometheus/exporters/mktxp/compose.d
ExecStart=/usr/bin/podman-compose up -d
ExecStop=/usr/bin/podman-compose down
TimeoutStopSec=30s

[Install]
WantedBy=default.target
  

有効化は user scope で行う。

  systemctl --user daemon-reload
systemctl --user enable --now /opt/containers/systemd/rootless/mktxp.service
loginctl enable-linger "$USER"
  

ここで loginctl enable-linger を忘れると、再起動後やログイン状態で挙動が揺れやすい。rootless を常駐で扱う前提なら、ここは最初に固定しておいた方が良い。

Prometheus の scrape 設定(参考)

Prometheus 側はシンプルで、smartctl-exporter:9633 で返すようになれば scrape は普通に足せる。

  scrape_configs:
  - job_name: "smartctl"
    scrape_interval: 60s
    scrape_timeout: 10s
    static_configs:
      - targets: ["storage-server:9633"]
  

ここは以前の node_exporter 展開メモと同じ考え方で、Prometheus が中央にいて各 exporter を pull するだけだ。つまり複雑なのは scrape 側ではなく exporter 側の安定化であり、そこを分離して考えられると作業が軽くなる。

Grafana クエリ(nvme0 / nvme1 まとめて)

Grafana 側はデバイス固定のフィルタを残さないことを意識した。まとめて見る時のクエリは以下で十分だった。

  smartctl_device{device=~"nvme.*"}
# 温度例
smartctl_temperature_celsius{device=~"nvme.*"}
# 健康
smartctl_health_ok{device=~"nvme.*"}
  

一度 device="nvme1" のような固定条件を入れると、nvme0 を戻したあとに気付かず片方だけを見続けることがある。exporter を直したのにダッシュボードが古いまま、というズレを避けるためにも、ここは早めに見直しておくべきだった。

よくある詰まりと瞬殺コマンド

運用中に詰まるポイントは限られていたので、その場で叩くコマンドもまとめておいた。

  # /metricsが固まる→nvme0でブロックのサイン。まず除外して疎通回復
# その後ラッパーで戻すのが安全。

# コンテナがStopping状態で動かない
podman rm -f smartctl-exporter || true

# 名前重複エラーで起動できない
podman run --replace -d --name smartctl-exporter ...

# 9633が開いているか
ss -ltnp | grep :9633

# exporterがnvme0/1を何台見てるか
podman logs smartctl-exporter | sed -n 's/.*Number of devices found.*/&/p'
podman logs smartctl-exporter | grep -E 'readjson|Invalid Log Page|device not found|Listening on'
  

特に Stopping に張り付くケースやコンテナ名の衝突は、原因を深追いする前に一旦状態を戻した方が早い。監視系は「完全に美しい状態」より「すぐ観測可能に戻す」ことを優先した方がいい場面が多い。

rootless / rootful の依存をどう扱うか

ここは今回の運用メモで一番重要だった。systemd は同一スコープ内、つまり system 同士か user 同士でしか素直に依存を扱えない。rootful と rootless をまたぐ厳密な依存を組もうとすると、理屈の割に挙動が読みにくくなる。

だから方針は明確にした。

  • 依存は疎結合にして、ネットワーク越しの再試行で吸収する
  • 同一スコープ内だけ After= / Wants= / Requires= / PartOf= を使う
  • ポート重複は設計表で防ぐ

promtail -> lokiprometheus -> exporters も、接続失敗からの再試行が前提で成立する。ここを systemd の強依存で縛りすぎない方が、結果として復旧性が高い。

daemon-reload を忘れないための仕組み化

もうひとつのハマりどころは daemon-reload 忘れだった。unit や compose を直したのに、systemd 側は古い定義のまま動き続けると、変更が効いているのか効いていないのか分からなくなる。

そこで、まずは雑でもいいから sdreload を置くことにした。

  # /usr/local/bin/sdreload
#!/bin/sh
systemctl daemon-reload || true
systemctl --user daemon-reload || true
echo "[done] daemon-reload (system & user)"
chmod +x /usr/local/bin/sdreload
  

変更後の定型も明示する。

  # rootful(system scope)
sdreload
systemctl restart smartctl-exporter.service
systemctl status smartctl-exporter.service -n 30

# rootless(user scope)
sdreload
systemctl --user restart loki.service promtail.service
systemctl --user status loki.service promtail.service -n 30
  

これは高度な仕組みではないが、単純な抜け漏れを防ぐには十分だった。監視基盤では、こういう「毎回やる操作」を先に標準化しておく方が効果が大きい。

rootful / rootless 配置のベストプラクティス

配置ルール自体も明示した。

  • rootful(system): カーネルやデバイスに触るもの
  • rootless(user): アプリ層

具体的には以下のイメージになる。

  • rootful: smartctl-exporter, node-exporter
  • rootless: loki, prometheus, promtail, grafana

これは権限だけの話ではなく、障害時にどこを見に行くかの話でもある。ホスト依存の強い exporter を rootful へ寄せると、問題の切り分け先が明確になる。逆にアプリ層は user scope に閉じた方が、再起動や継続運転の扱いが軽い。

最終的な運用ルール

最終的に残した運用ルールは単純だった。

  • rootful と rootless をまたぐ依存は付けない
  • service / compose を触ったら必ず sdreload
  • ポートは設計表で固定して重複禁止
  • exporter は rootful、アプリは rootless を基本にする

このルールにしておくと、以前まとめた node_exporter の全台展開とも自然につながる。Prometheus の scrape 対象は増えても、運用の責務分離は崩れない。結果として、新しい exporter を足す時に毎回ゼロから判断しなくて済むようになった。

結果

今回のメモで得られたのは、smartctl-exporter の単発対処だけではなく、監視 exporter 全体をどう配置し、どう再起動し、どこで依存を切るかの基準だった。node_exporter を横に広げる時の考え方と、NVMe を扱う smartctl-exporter の安定化を同じ枠組みで扱えるようになったのは大きい。

少なくとも、rootful / rootless を曖昧に混ぜたまま運用するより、どの層が何を持つかを固定した方が復旧も説明も早い。監視基盤は細かいツール選定より、こうした運用の一貫性の方が効いてくると感じた。

今後

今後やることは明確で、既存の *.servicecompose/*.yml をこのルールへ寄せていくことだ。依存とポートの衝突しない最小セットへリライトできれば、同じ種類の詰まりはかなり減らせる。

smartctl-exporter に限れば、まず nvme1 で安定運用し、必要に応じて nvme0 を戻す。その上で Prometheus / Grafana 側の見え方を揃えれば、node_exporter と並んでハードウェア寄りの監視がかなり実用的になる。