はじめに
さくらインターネット Advent Calendar 2025 の22日目 シリーズ2となります。
この記事では、ホワイトボックスで用いるネットワークOS SONiCへ好きなツールを仕込みたいという欲求の元、内部で動くsystemdの仕組みを読み解いていきます。
なぜやるのか
私は普段さくらインターネットでGPUインフラのネットワーク設計に携わっており、その中で、SONiCを駆使する機会がありました。
SONiC自体オープンソースで日々成長中ということで、監視や運用に関する部分は、別途自分たちで手を入れたいという気持ちがありました。
今回は、その一環でツール(世にいうミドルウェア)を仕込むため、この取り組みが必要でした。
(お約束ですが、この記事は個人的な知識を元に再構築したメモのため、不明点があるときは個別にご連絡ください)
そもそもSONiCとは
SONiC(Software for Open Networking in the Cloud) は、MicrosoftがAzureのために開発したオープンソースのネットワークOSです。
現在は Microsoft から The Linux Foundation に寄贈され、コミュニティ主導で開発が続けられています。その特徴をざっくりまとめると下記のようになります。
- Debian Linux をベースにしたネットワークOS
- FRR / SWSS / redisなど複数のDockerコンテナで構成されている
- ASIC を抽象化する SAI(Switch Abstraction Interface) によってマルチベンダのスイッチに対応
入手方法は大きく分けて3つとなります。
- リポジトリから自分でbuildが可能です(最速) https://github.com/sonic-net/sonic-buildimage
- 非公式サイトからビルド済みイメージを入手可能です(少しゆっくり) https://sonic.software/
- 商用版を購入する(communityを元に独自の拡張がされている/個別問い合わせ)
systemdを読んでみる
SONiCはDebianをベースとしているため、ソフトウェアの起動順序もLinuxを由来としたsystemdベースで管理がされています。
実際に下記コマンドでその起動順(依存関係)を確認することができます。
$ systemctl list-dependencies
default.target
● ├─display-manager.service
● ├─e2scrub_reap.service
● ├─kexec-load.service
● ├─kexec.service
● ├─monit.service
● ├─sysfsutils.service
● ├─systemd-update-utmp-runlevel.service
● └─multi-user.target
~snip~
● └─sonic.target
● ├─aaastatsd.timer
● ├─backend-acl.service
● ├─bgp.service
● ├─caclmgrd.service
● ├─copp-config.service
● ├─ctrmgrd.service
● ├─dhcp_relay.service
● ├─eventd.service
● ├─gbsyncd.service
● ├─hostcfgd.timer
● ├─hostname-config.service
● ├─iccpd.service
● ├─interfaces-config.service
● ├─macsec.service
● ├─mux.service
● ├─nat.service
● ├─node_exporter.service
● ├─ntp-config.service
● ├─pmon.service
● ├─procdockerstatsd.service
● ├─radv.service
● ├─rsyslog-config.service
● ├─service-restart.service
● ├─sflow.service
● ├─stp.service
● ├─swss.service
● ├─syncd.service
● ├─tacacs-config.timer
● └─teamd.service
例としてbgp.service(ルーティング処理をするFRR)のunit fileを開くと、下記の通り記載されています。
$ cat /etc/systemd/system/sonic.target.wants/bgp.service
[Unit]
Description=BGP container
Requires=database.service
After=database.service
Requires=updategraph.service
After=updategraph.service
BindsTo=sonic.target
After=sonic.target
Before=ntp-config.service
After=swss.service
After=pmon.service
After=interfaces-config.service
StartLimitIntervalSec=1200
StartLimitBurst=3
[Service]
User=admin
ExecStartPre=/usr/local/bin/bgp.sh start
ExecStart=/usr/local/bin/bgp.sh wait
ExecStop=/usr/local/bin/bgp.sh stop
ExecStopPost=/usr/local/bin/write_standby.py --shutdown bgp
RestartSec=30
[Install]
WantedBy=sonic.target
この結果を見ると、sonic.targetにFRRを始めとした内部サービスがぶら下がっていることが、なんとなく判ります。
(実際にはこのbgp.shの中身も読んで判断しています)
言い換えれば、sonic.target配下のサービスが起動するまで、起動は終わっていないということです。
そのため、新しいツールの起動は終わるまで待った方が無難そうです。
今回追加するツールもsonic.targetの後に起動するようにします。
systemdにnode exporterを登録
今回、例としてPrometheusのnode exporterを新規サービスとして登録してみます。
/usr/local/bin/にbinaryを配置した前提で、systemdで起動の設定をします。
- Before: 事前起動
- After : 該当サービスが起動後に起動
上記を元にして、下記のunit fileを作成してみます。
$ cat /etc/systemd/system/node_exporter.service
[Unit]
Description=Prometheus Node Exporter
After=sonic.target
[Service]
ExecStart=/usr/local/bin/node_exporter
Restart=always
[Install]
WantedBy=sonic.target
sonic関連のサービス(sonic.target)の後に起動してあげる、という記載です。
上記を作成したら、systemdのunitの再読み込みをすることで、登録したサービスをsystemdで管理できるようになります。
以下で、サービスの起動、bootup時の起動を有効にします。
$ sudo systemctl daemon-reload
$ sudo systemctl start node_exporter
$ sudo systemctl enable node_exporter
念のためstatusを確認すると、下記のように無事登録されているのがわかります。
$ sudo systemctl status node_exporter
● node_exporter.service - Prometheus Node Exporter
Loaded: loaded (/etc/systemd/system/node_exporter.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2025-12-17 09:16:43 JST; 26s ago
Main PID: 2915419 (node_exporter)
Tasks: 9 (limit: 18504)
Memory: 11.9M
CGroup: /system.slice/node_exporter.service
└─2915419 /usr/local/bin/node_exporter
ちなみに
先ほど例として出したbgp.serviceをはじめ、各ユニットファイルは下記の jinja templateを元にして生成されています。
この記事を読んだそこのあなたが、より深くSONiCのsystemdについて理解したいと思ったなら、そのときはここを読むと理解が早いかもしれません。
sonic-buildimage(files/build_templates)
動作確認
最後にprometheus相当のLinuxサーバーからcurlでアクセス確認をしてみました。
無事動作してnode exporterによる監視が実現できることがわかります!
$ curl http://a.b.c.d:9100/metrics
# HELP bfd_peers Metric read from /var/tmp/textfile/bfd.prom
# TYPE bfd_peers gauge
bfd_peers{Interface="Ethernet0",Local_Addr="fe80::e201:a6ff:fedc:9f03",Peer_Addr="fe80::922d:77ff:fe14:3100",State="up",Type="dynamic",Vrf="default"} 1
bfd_peers{Interface="Ethernet12",Local_Addr="fe80::e201:a6ff:fedc:9f03",Peer_Addr="fe80::1644:8fff:febc:d9c8",State="up",Type="dynamic",Vrf="default"} 1
bfd_peers{Interface="Ethernet4",Local_Addr="fe80::e201:a6ff:fedc:9f03",Peer_Addr="fe80::922d:77ff:fe14:2a00",State="up",Type="dynamic",Vrf="default"} 1
bfd_peers{Interface="Ethernet8",Local_Addr="fe80::e201:a6ff:fedc:9f03",Peer_Addr="fe80::922d:77ff:fe14:2600",State="up",Type="dynamic",Vrf="default"} 1
# HELP bgp_neighbors Metric read from /var/tmp/textfile/bgp.prom
# TYPE bgp_neighbors gauge
bgp_neighbors{Neighbhor="Ethernet0",NeighborName="NotAvailable",RemoteAS="4200000101",StatePfxRcd="1",UpDown="12w6d19h",Version="4"} 1
bgp_neighbors{Neighbhor="Ethernet100",NeighborName="NotAvailable",RemoteAS="0",StatePfxRcd="Idle",UpDown="never",Version="4"} 1
bgp_neighbors{Neighbhor="Ethernet104",NeighborName="NotAvailable",RemoteAS="0",StatePfxRcd="Idle",UpDown="never",Version="4"} 1
bgp_neighbors{Neighbhor="Ethernet108",NeighborName="NotAvailable",RemoteAS="0",StatePfxRcd="Idle",UpDown="never",Version="4"} 1
bgp_neighbors{Neighbhor="Ethernet112",NeighborName="NotAvailable",RemoteAS="0",StatePfxRcd="Idle",UpDown="never",Version="4"} 1
bgp_neighbors{Neighbhor="Ethernet116",NeighborName="NotAvailable",RemoteAS="0",StatePfxRcd="Idle",UpDown="never",Version="4"} 1
~snip~
# HELP pmon_service pmon service
# TYPE pmon_service gauge
pmon_service{status="active"} 1
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 6.86
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 524288
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 10
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 2.3662592e+07
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.7659313628e+09
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1.27123456e+09
# HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes.
# TYPE process_virtual_memory_max_bytes gauge
process_virtual_memory_max_bytes 1.8446744073709552e+19
# HELP promhttp_metric_handler_errors_total Total number of internal errors encountered by the promhttp metric handler.
# TYPE promhttp_metric_handler_errors_total counter
promhttp_metric_handler_errors_total{cause="encoding"} 0
promhttp_metric_handler_errors_total{cause="gathering"} 0
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 34
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0
# HELP radv_service radv service
# TYPE radv_service gauge
radv_service{status="active"} 1
# HELP routesTotal Metric read from /var/tmp/textfile/route-summary.prom
# TYPE routesTotal untyped
routesTotal{vrf="default"} 6
# HELP routesTotalFib Metric read from /var/tmp/textfile/route-summary.prom
# TYPE routesTotalFib untyped
routesTotalFib{vrf="default"} 6
# HELP sflow_service sflow service
# TYPE sflow_service gauge
sflow_service{status="active"} 1
# HELP snmp_service snmp service
# TYPE snmp_service gauge
snmp_service{status="active"} 1
# HELP swss_service swss service
# TYPE swss_service gauge
swss_service{status="active"} 1
# HELP syncd_service syncd service
# TYPE syncd_service gauge
syncd_service{status="active"} 1
# HELP teamd_service teamd service
# TYPE teamd_service gauge
teamd_service{status="active"} 1
なお端折っていますが、予め社内からACL(iptables)の許可はしていますので、ここは各自の環境に合わせてご注意ください。
まとめ
SONiCのsystemdを眺めて自分たちでツールを追加することができました。
この取り組みにより、node exporterをはじめ、監視関連のツールを自分たちで追加できるようになったのはよかったです。
今回は、SNMPではちょっと足りない、けど、TelemetryのCollectorを立てるほどではない、そんな中間地点の監視例として、node exporterで実現してみました。
ですが、本質的なベネフィットは、SONiCのベースとなっている Debianで動くツールであれば、Linuxの流儀で追加できる と判ったことだと思います。
今回は事情あってbinaryを動かす例を挙げましたが、せっかくのSONiCなので、Dockerコンテナ中心としたツール実装を今後検討できればと思います。
併せて、SONiCのredisを直接読んで、node exporterでは対応しきれないようなツールを作れたら面白そうです。
もしいい活用法など思いつかれましたら、是非コメントまでお寄せください!
おまけ
実は、この記事はネタが絞り切れず書いてしまった2本目の記事です。(後から満席になっているのを見て申し訳ない限り)
せっかくなので、もう一本の記事もよろしければご覧ください。