【保存版】Synology NASのステータスをWebで公開してみた|DSM APIに全力で玉砕してから/proc直読みで完全解決した話

Synology

自分のNASのステータスを、ブログのどこかに「こっそり」表示しておいたら、ちょっとかっこよくないですか?

CPU負荷がいくつで、ドライブが全台健康で、稼働時間が何日で……みたいな情報をリアルタイムでWebに流す。エンジニアっぽい雰囲気がにじみ出てくる、あのやつです。

というわけで、僕のSynology NASのステータスをWebサイトにリアルタイム表示する仕組みを構築してみました。

ところが……。想定外の敵が立ちはだかりました。

「Synology公式APIが動かない……」

APIの名前を間違えていたのかと思って調べたら988件のAPIが返ってきて、そこから正しいAPI名を特定して、いろんな認証方式を全部試したのに、最後まで error 101 という壁に阻まれました。

最終的な解決策は、「NASはLinuxなんだから /proc 直接読めばいいじゃないか」という、ある意味シンプルすぎる発想でした。

結果として、APIより多くの情報をよりシンプルに取得することに成功。今回はその全顛末をお届けします。


目次

  1. 完成形の全体像
  2. なぜCloudflare Tunnelを使うのか
  3. STEP 1:Synology DSM APIへの挑戦(と敗北)
  4. STEP 2:PHPでLinux /proc を直読みする
  5. /proc/mdstat の解析術|Synology固有の罠
  6. ネットワークスループットの出し方
  7. 完成したPHPコード(最終版)
  8. フロントエンドのJavaScript
  9. Cloudflare Tunnelのセットアップ手順
  10. トラブルシューティング集
  11. まとめ

完成形の全体像

まず、最終的に動いている構成から見ておきましょう。

NAS上のPHP(/proc読み取り)→ Cloudflare Tunnel → 公開URL → Webサイトのfetch

表示できるデータはこちら。すべてリアルタイムです。

  • ストレージ総容量・使用率(97.7 TBのうち何TB使っているか)
  • 8台のHDDドライブの個別健康状態(RAID5構成)
  • CPU負荷(ロードアベレージ)
  • メモリ使用率
  • ネットワークスループット(受信/送信 MB/s)
  • システム稼働時間

Kay’s Voice: ダッシュボードに「75日 22時間 33分」って稼働時間が表示されているのを見ると、なんか育てているペットを見守っている気分になります。落ちるなよ……。


なぜCloudflare Tunnelを使うのか

NASはルーター配下にあるプライベートIPしか持っていないため、外部からアクセスするには何らかの仕組みが必要です。選択肢はこの3つ。

方法セキュリティ手間
ポート開放(DDNS)⚠️ 高リスク(NASが直接攻撃対象になる)
VPN経由✅ 低リスク❌ 高い(訪問者もVPN必要)
Cloudflare Tunnel✅ 低リスク(NASは外部から到達不可)✅ 低い

Cloudflare Tunnelは、NAS側の cloudflared プロセスがCloudflareのエッジに向かってアウトバウンド接続を張り続ける仕組みです。外部からNASへ直接アクセスするポートは不要で、TLS終端もCloudflare側がやってくれます。

つまり、NASのポートをインターネットに開放しなくてよい。

🔍 Kay’s Check: Cloudflare Tunnelを使うと、NASにアクセスするための穴(ポート)をルーターに開けなくてよくなります。外から誰かが直接NASを叩けない状態を保ちつつ、Webサイトからはデータを取得できる——というセキュリティ的にほぼ理想的な構成です。


STEP 1:Synology DSM APIへの挑戦(と敗北)

まずAPIの仕組みを理解する

Synology DSMはREST形式のWeb APIを提供しています。流れはシンプルで、

  1. SYNO.API.Auth でログイン → セッションID(SID)を取得
  2. SIDを付けて各APIを呼び出す

これを PHP の file_get_contents で叩く実装を書き始めました。認証専用ユーザー ITI-API を作成して administratorsグループに追加。準備万端のはずでした。

API名が988件の中に埋もれていた

最初に試した SYNO.Storage.CGI.Disk は error 102(API not found)。SYNO.Storage.CGI.Storage も同様。

「あれ、API名が違う?」

正しいAPI名を調べるために、全API一覧を取得したところ……988件 返ってきました。

php

$info = file_get_contents(
    "http://localhost:5000/webapi/query.cgi" .
    "?api=SYNO.API.Info&version=1&method=query&query=all&_sid=$sid"
);
// → 全988件のAPIが返ってくる

988件の中からストレージ関連を絞り込んだ結果、正しいAPI名はこちらでした。

SYNO.Core.Storage.Disk    ← ディスク情報
SYNO.Core.Storage.Volume  ← ボリューム情報
SYNO.Core.Storage.Pool    ← プール/RAID情報

これらは entry.cgi 経由でアクセスします。「よし、これで動く!」

そしてerror 101の壁

GET形式               → error 101
POST JSON形式         → error 101
SynoToken(CSRF対策) → error 101
additionalパラメータ  → error 101

……全滅でした。

正しいAPI名で、正しい認証方法で、全パターン試しても error 101(Invalid parameter)が返り続けました。

「これ、僕の使い方が悪いのか、それともそもそも動かないのか……」

最終的な推測はこうです。SYNO.Core.* 系のAPIは、DSM管理画面のブラウザセッション専用で動作しており、CSRF対策以上の何らかのセッション検証が走っているか、Storage Manager専用のアプリケーション権限が別途必要なのかもしれません。断定はできませんでしたが、ここで撤退を決意しました。

結論:Synology公式APIは最終的に使っていません。


STEP 2:PHPでLinux /proc を直読みする

方針を切り替えました。

Synology DSMはLinuxカーネルの上に独自UIを載せた構成です。WebStationで動くPHPはNASのローカルファイルシステムにアクセスできる——つまり /proc ファイルシステムが読める、ということに気づきました。

🔍 Kay’s Check: /proc は「procファイルシステム」と呼ばれる、Linuxカーネルが提供する仮想的なファイルシステムです。実際にディスクに書かれているわけではなく、カーネルが今この瞬間の状態をファイルとして見せてくれる仕組みです。CPUの負荷やメモリの残量、ネットワークの通信量などがここに格納されています。Synologyのような組み込みLinux機器でも、同じ仕組みが使えます。

取得できる情報をまとめると、こうなります。

情報ソース
ボリューム総容量・空き容量disk_total_space() / disk_free_space()
RAIDステータス・ドライブ健康状態/proc/mdstat
CPU負荷(ロードアベレージ)sys_getloadavg()
メモリ使用量/proc/meminfo
ネットワーク累積トラフィック/proc/net/dev
システム稼働時間/proc/uptime
ドライブ温度/sys/class/thermal(権限不足で断念)

PHPのビルトイン関数で取れるものはそれで取り、残りは /proc 以下のテキストファイルを file_get_contents で読んで正規表現で解析するだけ。APIのセッション管理とか認証トークンとか、そういう面倒な話が全部なくなりました。

Kay’s Voice: DSM APIで988件の中から正しいAPI名を探して、error 101と格闘していた数時間はなんだったのか……。でも、回り道しないとこの解法にたどり着けなかったので、まぁよしとします。


/proc/mdstat の解析術|Synology固有の罠

ドライブの健康状態は /proc/mdstat から読み取ります。このファイルはLinuxソフトウェアRAID(md)の状態を表しています。

実際の /proc/mdstat の中身はこんな感じです(8台RAID5の場合)。

md2 : active raid5 sata1p5[0] sata8p5[7] sata7p5[6] ... sata2p5[1]
      109306083712 blocks super 1.2 level 5, 64k chunk [8/8] [UUUUUUUU]

[UUUUUUUU] の U がそれぞれのドライブの状態で、U=正常、_=障害です。8台全部 U なら全部生きています。

Synologyだと正規表現がそのまま使えない

ここで最初にハマりました。一般的なLinuxのRAIDデバイス名は sdasdbsdc……となるのですが、Synologyでは sata1sata2……sata8 という独自の命名規則になっています。

php

// ❌ 一般的なLinux向け正規表現(Synologyでは動かない)
preg_match_all('/sd([a-z]+)\[(\d+)\]/', $mdstat, $m);

// ✅ Synology用(sata番号で書く)
preg_match_all('/sata(\d+)p\d+\[(\d+)\](\(F\)|\(S\))?/', $block, $m);

ドライブが障害を起こしたときのフラグも確認できます。

  • (F) = Failed(障害ドライブ)
  • (S) = Spare(ホットスペア)

🔍 Kay’s Check: /proc/mdstat の解析で一番ハマりやすいのが、このデバイス名の差異です。Webで見つかるサンプルコードの多くは sda/sdb を前提にしているため、Synologyではそのまま動きません。sata1〜sata8 を前提にした正規表現に書き換えることが必須です。


ネットワークスループットの出し方

/proc/net/dev はネットワークインターフェースごとの累積バイト数しか持っていません。「今の転送速度(MB/s)」を出すには、前回値との差分を計算する必要があります。

/proc/net/dev の例:
eth0: 3663582201582  ... 650931701580 ...
      └── 受信累積バイト      └── 送信累積バイト

実装は、前回取得した値を /tmp/iti_net_prev.json に保存しておき、次回アクセス時に差分を計算する方式です。

php

$dt = $now - $prev['t'];  // 前回からの経過秒数
if ($dt > 0 && $dt < 300) {  // 5分以内のデータのみ有効
    $rx_mbps = ($curr_rx - $prev['rx']) / $dt / 1024 / 1024;
    $tx_mbps = ($curr_tx - $prev['tx']) / $dt / 1024 / 1024;
}

PHPのキャッシュが60秒なので、実質「過去60秒間の平均スループット」が表示されます。


完成したPHPコード(最終版)

/volume1/web/iti-api/nas-status.php に配置します。

php

<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: max-age=60');

// APIキー認証
$api_key = $_GET['key'] ?? '';
if ($api_key !== 'YOUR_API_KEY') {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// 60秒ファイルキャッシュ
$cache_file = '/tmp/iti_nas_status.json';
if (file_exists($cache_file) && (time() - filemtime($cache_file)) < 60) {
    echo file_get_contents($cache_file);
    exit;
}

// ── ボリューム容量(PHPビルトイン) ──
$total    = disk_total_space('/volume1') ?: 0;
$free     = disk_free_space('/volume1')  ?: 0;
$used     = $total - $free;
$total_tb = round($total / (1024 ** 4), 1);
$used_tb  = round($used  / (1024 ** 4), 1);
$used_pct = $total > 0 ? round($used / $total * 100, 1) : 0;

// ── ドライブ健康状態(/proc/mdstat) ──
$drives = []; $vol_status = 'normal';
$mdstat = @file_get_contents('/proc/mdstat');
if ($mdstat) {
    preg_match('/^md2\s*:.*?(?=^md[0-9]|\z)/ms', $mdstat, $md2);
    $block = $md2[0] ?? $mdstat;
    preg_match_all('/sata(\d+)p\d+\[(\d+)\](\(F\)|\(S\))?/', $block, $m);
    preg_match('/\[\d+\/\d+\]\s*\[([U_]+)\]/', $block, $health);
    $health_str = $health[1] ?? str_repeat('U', 8);
    $drive_map  = [];
    for ($i = 0; $i < count($m[0]); $i++) {
        $sata_num = (int)$m[1][$i];
        $position = (int)$m[2][$i];
        $flag     = $m[3][$i] ?? '';
        $up       = $health_str[$position] ?? 'U';
        $status   = ($flag === '(F)' || $up === '_') ? 'failed'
                  : ($flag === '(S)' ? 'spare' : 'normal');
        $drive_map[$sata_num] = ['slot' => $sata_num, 'status' => $status];
    }
    ksort($drive_map);
    $drives = array_values($drive_map);
    for ($i = count($drives) + 1; $i <= 8; $i++) $drives[] = ['slot'=>$i,'status'=>'normal'];
    if (strpos($health_str, '_')      !== false) $vol_status = 'degraded';
    if (strpos($mdstat, 'recovering') !== false) $vol_status = 'recovering';
    if (strpos($mdstat, 'resync')     !== false) $vol_status = 'resyncing';
} else {
    for ($i = 1; $i <= 8; $i++) $drives[] = ['slot'=>$i,'status'=>'normal'];
}

// ── CPU負荷 ──
$load      = sys_getloadavg();
$cpuinfo   = @file_get_contents('/proc/cpuinfo');
$cpu_cores = $cpuinfo ? max(1, preg_match_all('/^processor\s*:/m', $cpuinfo)) : 4;
$cpu_pct   = round(min(100, ($load[0] / $cpu_cores) * 100), 1);

// ── メモリ ──
$meminfo      = @file_get_contents('/proc/meminfo');
$mem_total_kb = 0; $mem_avail_kb = 0;
if ($meminfo) {
    preg_match('/MemTotal:\s+(\d+)/', $meminfo, $mt);
    preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $ma);
    $mem_total_kb = (int)($mt[1] ?? 0);
    $mem_avail_kb = (int)($ma[1] ?? 0);
}
$mem_used_pct = $mem_total_kb > 0
    ? round(($mem_total_kb - $mem_avail_kb) / $mem_total_kb * 100, 1) : 0;

// ── ネットワークスループット(差分計算) ──
$rx_mbps = 0; $tx_mbps = 0;
$net_prev = '/tmp/iti_net_prev.json';
$netdev   = @file_get_contents('/proc/net/dev');
if ($netdev && preg_match(
    '/^\s*eth0:\s*(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/m',
    $netdev, $n)) {
    $curr_rx = (int)$n[1]; $curr_tx = (int)$n[2]; $now = time();
    if (file_exists($net_prev)) {
        $prev = json_decode(file_get_contents($net_prev), true);
        $dt   = $now - ($prev['t'] ?? 0);
        if ($dt > 0 && $dt < 300) {
            $rx_mbps = max(0, round(($curr_rx - ($prev['rx'] ?? 0)) / $dt / 1024 / 1024, 2));
            $tx_mbps = max(0, round(($curr_tx - ($prev['tx'] ?? 0)) / $dt / 1024 / 1024, 2));
        }
    }
    file_put_contents($net_prev, json_encode(['t'=>$now,'rx'=>$curr_rx,'tx'=>$curr_tx]));
}

// ── 稼働時間 ──
$uptime_raw = @file_get_contents('/proc/uptime');
$uptime_sec = $uptime_raw ? (int)explode(' ', $uptime_raw)[0] : 0;
$uptime_str = intdiv($uptime_sec, 86400) . 'd '
            . intdiv($uptime_sec % 86400, 3600) . 'h '
            . intdiv($uptime_sec % 3600, 60) . 'm';

$result = [
    'ok'      => true,
    'volume'  => ['total_tb'=>$total_tb,'used_tb'=>$used_tb,'used_pct'=>$used_pct,'status'=>$vol_status],
    'drives'  => $drives,
    'cpu'     => ['load_1m'=>round($load[0],2),'cores'=>$cpu_cores,'pct'=>$cpu_pct],
    'memory'  => ['used_pct'=>$mem_used_pct,'total_gb'=>round($mem_total_kb/1024/1024,1)],
    'network' => ['rx_mbps'=>$rx_mbps,'tx_mbps'=>$tx_mbps],
    'uptime'  => $uptime_str,
];

$json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
file_put_contents($cache_file, $json);
echo $json;

APIを叩くと、こんなJSONが返ってきます。

json

{
    "ok": true,
    "volume":  { "total_tb": 97.7, "used_tb": 35.8, "used_pct": 36.7, "status": "normal" },
    "drives":  [ { "slot": 1, "status": "normal" }, ... ],
    "cpu":     { "load_1m": 1.27, "cores": 4, "pct": 31.8 },
    "memory":  { "used_pct": 24.6, "total_gb": 32.0 },
    "network": { "rx_mbps": 3.4,  "tx_mbps": 1.2 },
    "uptime":  "75d 22h 33m"
}

🔍 Kay’s Check: コード中の YOUR_API_KEY は必ず変更してください。これを知っている人だけがAPIにアクセスできます。完璧なセキュリティではありませんが、NASのポートが外部に露出していないこと・PHPは読み取りのみで書き込みやシェル実行がないことで、リスクは最小限に抑えられています。


フロントエンドのJavaScript

Webページ側は fetch でJSONを取得してDOM要素を更新するだけです。ページ読み込み時に1回だけ実行し、NASがオフラインでも静的表示のままエラーにならないようにしています。

javascript

(async function fetchNasStatus() {
  try {
    const res  = await fetch('https://iti-api.itis.co.jp/nas-status.php?key=YOUR_API_KEY');
    const data = await res.json();
    if (!data.ok) return;

    // ストレージ
    document.getElementById('nas-total-tb').textContent = data.volume.total_tb + ' TB';
    document.getElementById('nas-used-pct').textContent = data.volume.used_pct + '%';

    // ヘッダーステータス
    const allHealthy = data.drives.every(d => d.status === 'normal');
    const label = data.volume.status === 'normal' && allHealthy
        ? 'ALL DRIVES HEALTHY'
        : data.volume.status === 'degraded' ? 'DEGRADED — CHECK DRIVES' : 'RESYNCING';
    document.getElementById('nas-status-text').textContent = 'SYNOLOGY NAS — ' + label;

    // ドライブバー(障害時は赤く)
    data.drives.forEach(dr => {
      const el = document.getElementById('nas-drive-' + dr.slot);
      if (!el) return;
      if (dr.status === 'failed') {
        el.style.background = 'linear-gradient(90deg, #ff2244, #cc0022)';
        el.style.boxShadow  = '0 0 8px rgba(255,34,68,0.6)';
      }
    });

    // CPU(70%超で橙、90%超で赤)
    const cpuBar = document.getElementById('nas-cpu-bar');
    cpuBar.style.width = data.cpu.pct + '%';
    cpuBar.className   = 'nas-sys-fill'
        + (data.cpu.pct > 90 ? ' alert' : data.cpu.pct > 70 ? ' warn' : '');
    document.getElementById('nas-cpu-val').textContent = data.cpu.load_1m + ' avg';

    // メモリ
    const ramBar = document.getElementById('nas-ram-bar');
    ramBar.style.width = data.memory.used_pct + '%';
    ramBar.className   = 'nas-sys-fill'
        + (data.memory.used_pct > 90 ? ' alert' : data.memory.used_pct > 75 ? ' warn' : '');
    document.getElementById('nas-ram-val').textContent = data.memory.used_pct + '%';

    // ネットワーク速度(MB/s or KB/s で自動切替)
    const fmt = mb => mb >= 1 ? mb.toFixed(1) + ' MB/s'
                   : mb > 0   ? (mb * 1024).toFixed(0) + ' KB/s' : '0 B/s';
    document.getElementById('nas-rx-speed').textContent = fmt(data.network.rx_mbps);
    document.getElementById('nas-tx-speed').textContent = fmt(data.network.tx_mbps);

    // 稼働時間
    document.getElementById('nas-uptime').textContent = data.uptime;

  } catch(e) { /* NASオフライン時は静的表示のまま */ }
})();

Cloudflare Tunnelのセットアップ手順

前提条件

  • Cloudflareにドメインを追加済み(DNSのNSをCloudflareに変更している)
  • SynologyパッケージセンターからCloudflaredをインストール済み

Zero TrustでトンネルとPublic Hostnameを設定する

  1. dash.cloudflare.com → Zero Trust → Networks → Tunnels
  2. 「Create a tunnel」→ Cloudflaredを選択
  3. NAS上でコネクターを起動(パッケージセンターから自動化できます)
  4. Public Hostname を追加する
設定項目
Subdomainiti-api
Domainitis.co.jp
Service TypeHTTP
URLlocalhost:80

保存するとCloudflareが自動的にCNAMEレコードを作成します。

WebStationでバーチャルホストを設定する

DSM → Web Station → Web Portal → 「作成」:

設定項目
ポータルタイプ名前ベース
ホスト名iti-api.itis.co.jp
ポート80(HTTP)
バックエンドサーバーPHP 8.2 サービス
ドキュメントルート/volume1/web/iti-api

🔍 Kay’s Check: Cloudflare TunnelはNASにHTTP(port 80)で接続します。WebStationのポータルもHTTPを受け付ける設定が必要です。「HTTPSじゃないの?」と思うかもしれませんが、Cloudflareがブラウザとの間でTLSを終端してくれるため、NAS側はHTTPのままで大丈夫です。


トラブルシューティング集

実際に詰まったポイントをまとめておきます。

①「API not found」が出る(error 102/103)

SYNO.Storage.CGI.Disk などのAPI名が間違っていることが多いです。SYNO.API.Info?query=all で全988件を取得し、ストレージ関連を検索して正しいAPI名を特定しましょう。正解は SYNO.Core.Storage.DiskSYNO.Core.Storage.Volume

②/proc/mdstatの正規表現がマッチしない

SynologyのRAIDデバイス名は sata1sata8 です。sda/sdb を前提にした正規表現はそのまま使えません。上記のサンプルコードを参考にしてください。

③Cloudflare Tunnelを追加したのにアクセスできない

WebStationに同名のバーチャルホストが設定されていないとルーティングされません。DSM → Web Station → Web Portal で対応するポータルを追加してください。

④ブラウザで接続できないがdigでは正しいIPが返る

macOSのDNSキャッシュが古いIPを返しているかもしれません。

bash

sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

まとめ

  • Synology DSM APIは最終的に使っていない。 SYNO.Core.* 系のAPIはerror 101が解消できず断念
  • NASはLinuxなので /proc が読める。 WebStationのPHPから直接読み取ることで、APIより多くの情報を取得できた
  • Cloudflare Tunnelで安全に公開。 NASのポートを外部に開放せずHTTPS公開できる
  • Synology固有の罠に注意。 /proc/mdstat のデバイス名は sata1sata8sda/sdb ではない)
  • ネットワーク速度は差分計算が必要。 /proc/net/dev は累積値なので、前回値をファイルに保存して計算する

自分のNASのステータスをWebに載せるだけで、なんだかサーバー管理者っぽい雰囲気が出てきます。ドライブが全台健康な「ALL DRIVES HEALTHY」の表示を見るたびに、「よし、今日もみんな元気だ」 という謎の安心感があります。

ぜひお試しください!

コメント

タイトルとURLをコピーしました