ブラウザ版の更新は 1 コマンド。差分は rsync に送らせ、root でできることは固定のスクリプト 1 本だけ
One command updates the browser version: rsync sends the diff, root can run only one fixed script
D17-3-T33-one-command-deploy-rsync・技術・日本語入力でプレイする仕組み
ブラウザ版の更新は、手元で 1 コマンドを流すだけです。パスワードは聞かれません。
送るのは変わった所だけで、サーバの上で root の権限で動くのは、引数を取らない固定のスクリプト 1 本だけにしてあります。
T30〜T32 で書いたサーバに、新しい版を置く話です。
送る物は、Linux 版の adv-sim、ONNX Runtime の共有ライブラリ、第一章までに切った作品データと焼いたベクトルとモデル、server.mjs と画面の一式。今日、手元に組まれていたもので 167 ファイル・178 MB です。
この回では、配置先のホスト名・IP・ユーザー名・パス・sudo の設定の中身は書きません。スクリプトの抜粋も、それらが写る所は <配置先> のように置き換えています。
この回の位置と、前後の回#
区切り: T30〜T33 配る仕組み(4 回のうち 4 回目)
先に読むとよい回: T30(状態を持たないサーバ)・T07(同じベクトルを出す ONNX Runtime)
次に読む回: T34(台本を実行せずに検査する)
名前 — rsync algorithm#
- 正式名
- the rsync algorithm(rsync の差分転送・delta-transfer algorithm)/rolling checksum(転がる checksum)
- もう一つ
- principle of least privilege(最小権限の原則)
初出:
rsync は Andrew Tridgell と Paul Mackerras の技術報告 "The rsync algorithm"(Joint Computer Science Technical Report TR-CS-96-05, The Australian National University, 1996)。
最小権限の原則は Saltzer & Schroeder "The Protection of Information in Computer Systems"(Proceedings of the IEEE, 1975)の設計の原則の一つです(1974 年の Communications of the ACM の版が先にあります)。
一般の解説(rsync の差分転送)#
遠くの 2 台に、少しだけ違う同じファイルがあるとします。違いだけを送りたいが、どこが違うかは、両方を見られる者がいないと分からない。
rsync の答えはこうです。
- 受け手(古いファイルを持つ側)は、ファイルを長さ S の重ならない塊に切り、塊ごとに 2 つの checksum を計算して送り手に送る。弱い 32bit の rolling checksum と、強いハッシュ(元の報告では 128bit の MD4)
- 送り手(新しいファイルを持つ側)は、自分のファイルのあらゆるバイトの位置で、長さ S の窓の弱い checksum を計算し、受け取った表と照らす。弱いものが合ったら強いハッシュでも確かめる
- 合った所は「受け手の何番の塊」という参照だけを送り、合わない所はそのままの中身(literal)を送る
- 受け手は、自分の古いファイルの塊と届いた中身から、新しいファイルを組み立てる
肝心なのは 2 の「あらゆるバイトの位置で」です。先頭に 1 バイト挿入されると、固定の位置で切った塊は全部ずれますが、1 バイトずつ窓を滑らせれば、ずれた後の塊も見つかります。
それを安くするのが rolling checksum です。報告の弱い checksum は adler-32 に着想を得たもので、窓 X_k..X_l について
a(k,l) = (Σ X_i) mod M
b(k,l) = (Σ (l − i + 1) X_i) mod M
s(k,l) = a(k,l) + 2^16 · b(k,l)
と置き(M = 2^16)、窓を 1 つ滑らせたときの a と b を、出て行く 1 バイトと入って来る 1 バイトだけから O(1) で更新できます。
弱い checksum はよく衝突するので、強いハッシュで確かめて初めて「同じ塊」とみなします。
実際の rsync の道具には、この算法の前に quick check があります。大きさと更新時刻が同じファイルは、中身を見ずに飛ばします。
また、同じ PC の中のフォルダどうしでは、既定で差分を計算せずファイルを丸ごと写します(--whole-file)。差分の計算より写す方が速いからです。
よく知られた弱点は、圧縮や暗号化で全体のバイトが変わるファイル(zip・暗号化した物)には効かないこと。小さな変更でも、後ろのバイトが全部変わるからです。
もう一つ、ファイルを差分で組み立てても、受け手は一時ファイルに書いてから入れ替えるので、受け手のディスクには丸ごと書きます。
一般の解説(最小権限の原則)#
Saltzer & Schroeder の原則の一つで、プログラムも利用者も、仕事を終えるのに要る最小の権限だけで動け、というものです。
事故や乗っ取りが起きたときに、できることの範囲を狭くしておくのが狙いです。
sudo で言えば、「この利用者はパスワード無しで何でもできる」ではなく、「この 1 つのコマンドだけを、この形でだけ」と書くのが、その原則に沿った使い方です。
うちの 1 コマンド#
入口は Scripts/adv-web-deploy.ps1 で、中身は WSL の bash の push.sh を呼ぶだけです。
$Root = Split-Path $PSScriptRoot -Parent
$drive = $Root.Substring(0, 1).ToLower()
$wslRoot = '/mnt/' + $drive + ($Root.Substring(2) -replace '\\', '/')
& wsl.exe -e bash -lc "bash '$wslRoot/Tools/adv-web/deploy/push.sh'"
exit $LASTEXITCODE
push.sh は 4 段で、末尾に PUSH OK か PUSH FAIL の 1 行を出します。
1/4 stage。WSL で Linux 版の adv-sim を増分でビルドし、配置先と同じ形のフォルダを手元に組む
2/4 rsync。差分だけを送る
3/4 apply。ssh で入り、パスワード無しの sudo で固定のスクリプトを 1 本呼ぶ
4/4 外から確認。公開の URL で /api/health と、シリーズの入口・案内ページ・ゲーム本体・作品の API の 4 つが 200 か
2 と 3 の行は、ホスト名を伏せてこう書いてあります。
rsync -az --delete --info=stats1 "$ROOT/Saved/adv-web-stage/" "<接続名>:<送り置き場>/" | { grep -E "^(Number of|Total)" || true; }
if ! ssh -o BatchMode=yes "<接続名>" 'sudo -n <固定のスクリプト>'; then
echo "PUSH FAIL: apply。初回なら install-apply.sh(README「VPS」)を先に 1 回"; exit 1
fi
BatchMode=yes と sudo -n は、どちらも「聞かれたら待たずに失敗する」です。パスワードを聞かれる状態なら、そこで PUSH FAIL になって止まります。人が画面の前にいなくても固まりません。
stage が確かめること#
1 段目の stage.sh は、組むだけでなく、配る前に止める検査を持っています。
- 作品を第 N 章までに切り落とす。ブラウザ版は第一章までで、後の章の意図も台本もサーバに置きません(T30 の「先の章を読まれないための守り」はここ)。切り落としと、図の作り直し・焼き直し・第一章の通しで章の終わりのフラグが立つことまでを 1 本のスクリプトがやり、CUT-CHECK PASS でなければ終了コード 1 で止まります。Steam の体験版を作るときも同じスクリプトです
- 画面は、公開してよい物だけを集める道具(build-site.mjs)で組む
- 送る組(このバイナリ・このデータ・この bake・共通の資源)のハッシュを manifest に書く。サーバは起動のたびにこれと照らし、違えば起動しません(T07)
- 組んだバイナリに、組んだデータと共有ライブラリで 1 手だけ答えさせてみる
- 版の札(git の短い SHA・作業ツリーが汚れているか・日時)を 1 行書く
固定のスクリプト#
サーバ側で root として動くのは、配置だけをするスクリプト 1 本です。パスを伏せて写します。
set -euo pipefail
STAGE=<送り置き場>
[ -f "$STAGE/adv-sim" ] || { echo "FAIL: 送付物が無い: $STAGE/adv-sim"; exit 2; }
[ "$(id -u)" -eq 0 ] || { echo "FAIL: root で実行する"; exit 2; }
prev="$(cat <配置先>/.version 2>/dev/null || echo '-')"
rm -rf <配置先>/*
cp -a "$STAGE"/. <配置先>/
chown -R <サービスのユーザー> <配置先>
chmod +x <配置先>/adv-sim …
systemctl restart adv-web
for i in $(seq 1 20); do
sleep 1
if curl -fsS http://127.0.0.1:<ポート>/api/health >/dev/null 2>&1; then
echo "APPLY OK: … (prev $prev) …"; exit 0
fi
done
echo "APPLY FAIL: adv-web が 20 秒で上がらない。…"
決めたことは三つです。
- 引数を取らない。送り元と置き先は中に書いてある
- ほかのコマンドを呼ばない。置いて、持ち主を変えて、実行の印を付けて、再起動して、20 秒待つだけ
- sudo の設定では、配置用のユーザーに、このスクリプト 1 本だけをパスワード無しで許す。ほかの root の操作は、今までどおりパスワードが要る
スクリプトと sudo の設定は、最初に 1 回だけ、パスワードを画面で入力して入れます(コマンドにパスワードを書かない)。
サービス自身は、ログインできない専用のユーザーで、systemd の隔離(NoNewPrivileges・ProtectSystem=strict・ProtectHome・PrivateTmp・書けるのはログの置き場だけ・メモリの上限)の下で動きます。
開発 AI にこの更新を任せるときも、同じ考え方です。開発 AI に許すのはこの 1 コマンドの 1 行だけで、scp・ssh・sudo を個別には許しません。
実測: rsync が何を送るか(今日・手元の WSL の中)#
今日は VPS に触っていないので、手元の WSL の中で、2 つのフォルダの間で測りました。同じ PC の中なので、時間はネットワークではなく計算とディスクの時間です。
同じ PC の中では rsync は既定で丸ごと写すので、--no-whole-file で差分の算法を使わせています。
① 空のフォルダへ(初回) 167 本 literal 178,032,182 B 13.42 s
② 中身は同じ・更新時刻だけ新しい 167 本 literal 0 B matched 178,032,182 B 20.59 s 送った 165,025 B
③ 同じものをもう一度 0 本 0.67 s 送った 4,738 B
④ 台本 1 本(2,789 B)に 1 字足した 1 本 literal 701 B matched 2,088 B 0.70 s 送った 5,502 B
⑤ 参考: ②と同じ条件で既定(whole-file) 167 本 literal 178,032,183 B 12.66 s
② が、いちばん大事な行です。
stage.sh は毎回、手元のフォルダを消して組み直します。cp は更新時刻を引き継がないので、中身が同じでも全部のファイルが「新しい」になります。
rsync の quick check(大きさと更新時刻)は、これを全部「変わった」と判断します。それでも送ったのは 178 MB のうち 165 KB でした。差分の算法が、全部の塊を「受け手の何番」と言い当てたからです。
代わりに、両側で全部のファイルの checksum を計算するので、時間は丸ごと写す ⑤ より長くかかっています。遠くのサーバに送るときは、この計算の時間より回線の時間の方がずっと大きいので、② の形で困ってはいません。
④ は、2,789 B の台本の中の 1 字の後ろに空白を 1 つ足しただけの変更です。literal 701 B は、変えた所を含む 1 つの塊(700 B)と、足した 1 B です。
差分の単位は行でも文字でもなく、塊です。
名前どおりでない所 — rsync algorithm#
rsync の算法は、うちが実装したものではありません。rsync の道具をそのまま使っています。名前どおりです。
ただし、使い方に二つの癖があります。
一つ目、quick check を毎回外してしまっています(② の形)。手元を毎回組み直すためで、更新時刻を引き継ぐ写し方(cp -p など)にすれば、変わらないファイルは算法を通る前に飛ばせます。いまの規模では差が小さいので、そのままにしています。
二つ目、送った後の配置は差分ではありません。固定のスクリプトは、配置先を消してから丸ごと写し直します。差分で送るのは回線の上だけです。
最小権限の原則も、名前どおりとは言い切れません。
root でできることは確かに 1 本に絞りましたが、そのスクリプトは「送り置き場にある物を、そのまま配置する」ので、送り置き場に書ける者は、どんな adv-sim でも置けます。置かれた物は root ではなく専用のユーザーで、隔離の下で動きますが、配置用の鍵を持つ者がサービスのユーザーとして任意のコードを動かせる、という範囲は残っています。
絞ったのは「root で何でもできる」から「配置して再起動できる」まで、です。
弱点と、採らなかった案 — rsync algorithm#
- 配置は原子的ではありません。消してから写し、再起動して上がるまでの数秒、ゲームの口はエラーを返します(前に立つ Caddy が中継先に届かない旨を返す)。T30 のとおりサーバは状態を持たないので、遊んでいる人はもう一度送れば続けられます
- 戻す手順は自動ではありません。前の版の札(prev)は出しますが、前の版の中身は残しません。戻すときは、手元で前の版を組み直して、同じ 1 コマンドを流します
- 今日、手元の stage の版の札は nogit でした。この作業場所(git の worktree)では、WSL の git が Windows の書き方のパスを読めず、SHA が取れていません。どの版を配ったかを札で追うなら、main の作業ツリーから組む必要があります
- 4 段目の確認は、URL が 200 を返すかだけです。中身が新しい版かは見ていません
採らなかった案:
- scp で毎回全部を送る。最初はそうしていました(手順書に残っています)。178 MB を毎回送り、実行の印が落ちる事故も踏みました
- VPS の上で git pull してビルドする。VPS はメモリ 1 GB で、C++ のビルドと ONNX Runtime を載せたくない。リポジトリには配らない物も多い
- CI(クラウドのビルドと配置)。配置の鍵を外のサービスに預けることになり、守る物が 1 つ増える
- 2 つの置き場を用意して、symlink の付け替えで切り替える(blue-green)。数秒の停止を無くせますが、いまの遊ばれ方では停止の数秒より、置き場が 2 倍になるメモリとディスクの方が気になる。ここは、まだやっていないだけ、とも言えます
検索語#
rsync algorithm Tridgell Mackerras rolling checksum
rsync quick check whole-file
principle of least privilege Saltzer Schroeder
sudoers NOPASSWD single command
この 1 コマンドで、『霧走町 流氷に眠る』の第一章のブラウザ版を更新しています。プロフィールのリンクから遊べます。