← シリーズに戻る

pull 型: 外から入れない機械と3つの通知

Part 2 / 6 Raspberry Pi デプロイ5方式を7軸比較

前の章の push 型と向きが逆になります。配る側はストレージに置くだけ。機械が自分で取りに来ます。

向いているのは、機械に入れない環境です。店舗やオフィスの LAN に置いた Raspberry Pi、4G 回線の先にある端末、NAT 配下に並んだ機器。「外から SSH できない」を前提にできるなら、この方式が第一候補になります。

置き場所は HTTP で読めればどこでも構いません。機械側に「取ってきて展開して反映する」プログラムを常駐させておき、それが定期的に、あるいは通知を受けて動く形になります。

ここまで読むと、SSH が要らなくなるぶん push 型より全面的に良いように見えます。そうではありません。7軸のうち上がるのは到達性とロールバック、下がるのは常駐コスト・観測性・権限の粒度で(帯域は固め方次第)、中でも push 型と大きく入れ替わるのは到達性・常駐コスト・観測性の3つです。機械の上で動き続けるプログラムが1つ増え、そのプログラムは特権を持ったまま動き続け、取りに来なかった機械を配る側から見つけられなくなります。置き場所そのものも、書き込み権限を取られたら全機械に任意のコードが流れる一点になります。

問題が消えたのではなく、場所が移っただけです。 どこに移ったのかを、以下7軸と、pull 型で新たに出てくる3つの論点(通知・取得の整合性・読み取り)で追います。

この方式の検証状態: 実機では動かしていません。以下は公式ドキュメントとプロトコル仕様から言える範囲です。実測が済んだセルは表を更新します。

pull 型の経路。配る側はストレージに置くだけで、機械の上のエージェントが HTTPS で取りに行く。起こし方は定期ポーリング・長時間ポーリング・MQTT 通知の3つで、遅れと常時接続のどちらを取るかが変わる。NAT の内側でも届く代わりに、エージェントが常駐し、取りに来なかった機械は配る側から成功と区別がつかない

到達性: この方式を選ぶ最大の理由

機械がどこに居ても動きます。家庭の LAN の内側でも、モバイル回線の先でも同じで、外向きに HTTPS が出られればそれで足ります。

モバイル回線がとくに効きます。通信事業者は加入者をまとめて NAT の内側に入れることがあり(キャリアグレード NAT と呼ばれます)、この下に居る機械にはグローバル IP がそもそも割り当てられません。契約者が設定を変えて解決できる話ではないので、外から入る前提の方式はここで完全に閉じます。

push 型が VPN や踏み台で解いていた問題が、ここではそもそも発生しません。工場出荷したあと顧客のネットワークに入ってしまう機械のように、こちらから触れない前提の相手には、実質この方式しか選べません。

通知(7軸の外): webhook が使えない場所での起こし方

到達できないということは、更新を知らせる手段も限られるということです。webhook は相手に接続しに行く仕組みなので、機械側から見て外から入れない時点で最初から候補に入りません。

起こし方は3つです。

起こし方反映までの遅れ常時つないでおくもの
定期ポーリング間隔ぶん(数分〜数時間)なし
長時間ポーリングほぼ即時HTTP 接続を張りっぱなし
機械側から張る常時接続(MQTT など)ほぼ即時ブローカーへの接続

3つ目のブローカーは、配る側と機械の間に立って通知を中継するサーバーです。機械のほうから接続を張りに行くので、外から入れない機械にも通知が届きます。webhook で解けなかった問題を、接続の向きを変えて解いている形です。

台数が数十までで、反映が数分遅れても構わないなら、定期ポーリングで足ります。実装が一番単純なうえに、失敗しても次の周期で勝手に再試行されるので、壊れにくさという一点では他の2つを頭一つ抜いています。

即時性が要る、あるいは台数が数千に増えると話が変わります。全機械が5分ごとに叩けばストレージ側から見えるのは分散しない山なので、ここで常時接続型に切り替える判断が出てきますが、ブローカーという常駐物が1つ増えるぶん、常駐コストの軸に跳ね返ります。

中間として、更新の有無だけを軽い問い合わせで確かめて、あったときだけ本体を取りに行く形もよく使われます。If-None-Match を付けて、変わっていなければ 304 が返る、というだけの仕組みでも、転送量はかなり削れます。

条件付きリクエストで、変わったときだけ本体を取る。機械はストレージに GET /config.tar を送るとき、前回の 200 で受け取った版の印 (ETag) を If-None-Match: "v41" として添える。変わっていなければストレージは 304 Not Modified を返し、本文は付かないので転送は応答ヘッダだけで終わり、機械はいま持っている版を使い続ける。変わっていれば 200 OK と新しい ETag: "v42" と本体を返し、このときだけ本体を取る。機械は新しい ETag を覚えておき、次の問い合わせではそれを添える (ETag の値とファイル名は例)

常駐コスト: 機械側に1つ増える

push 型が◎だったところが、こちらでは○か△になります。取得して展開して反映するプログラムが、機械の上で動き続けます。

そのプログラムが落ちたら更新は止まり、止まったことに気づく仕組みがまた別に要ります。設定を配りたいだけなのに、監視しなければならないものが1つ増えます。

代わりに得るものもあります。このプログラムは起動時にも取りに行けるので、数日オフラインだった機械が復帰したとき、push 型なら配る側が再実行しないと追いつかないところを、pull 型は誰も何もしなくても勝手に追いつきます。台数が増えるほど、この自己回復性の価値が上がります。

数日オフラインだった機械が戻ってきたときの比較。新しい版を出す時点で、push 型は CI が SSH で入りに行き、pull 型は置き場所に置くだけ。機械がオフラインの間、push 型は SSH が繋がらず CI が赤くなり、pull 型は誰も取りに来ないが、配る側からは成功と区別がつかない。機械が復帰すると、push 型は古い版のままで、配る側が再実行するまで追いつかない。pull 型はエージェントが起動時に取りに行くので、誰も何もしなくても追いつく。台数が増えるほど、この自己回復性の価値が上がる

権限の粒度: push 型と逆転する

見落としやすいところです。

push 型では、root 権限が使われるのは配っている数秒間だけでした。sudoers に列挙したコマンドが、CI が入っている間だけ実行されます。

pull 型では、常駐しているプログラムがその権限を持ったまま動き続けます。設定を /etc の下に置いてサービスを reload できる、ということは、そのプログラムが乗っ取られたら常にそれができるということです。

対策の方向は同じです。エージェント自体は一般ユーザーで動かし、特権が要る部分だけを sudoers で絞って呼ぶ。ただし「絞った権限を常に持っている」と「絞った権限を時々持つ」の差は残ります。この差は表の上では見えにくいので、意識して書き分ける必要があります。

同じように sudoers で列挙したコマンドしか通さない前提で、権限を使える時間を横に並べた模式図。push 型は CI が入っている間だけで、時間軸の上に配布のたびに短い区間が3つ立つだけ。root で動くのは配っている数秒間だけ。pull 型はエージェントが常駐している間ずっとで、時間軸の端から端まで帯が続く。乗っ取られたら、いつでも /etc の下に置いて reload できる。エージェントを一般ユーザーで動かし、特権の要る部分だけ sudoers で絞って呼んでも、絞った権限を常に持っていることと時々持つことの差は残る

ロールバック: 機械が自分で判断できる

ここは pull 型のほうが素直です。

取ってきたものを展開して、検証して、駄目なら自分で前の世代に戻せます。配る側との通信は要りません。世代をいくつか手元に残しておいて、シンボリックリンクを差し替える形にすれば、戻すのはリンクの張り替えだけで済みます。

push 型で「手続きの途中で死ぬ」と書いた問題が、こちらでは軽くなります。判断する主体が機械の上に居るからで、ネットワークが切れていても判断そのものは続きます。

取得の整合性(7軸の外): 半端なものを掴まない

前節の世代ディレクトリとシンボリックリンクは、戻すためだけの仕組みではありません。取得が途中で切れたときにも同じ形が効きます。

push 型では、転送してから配置するまでを CI が1つの手続きとして持っていました。pull 型は取得も配置も機械側なので、途中で切れたときに何が残るかを自分で決めることになります。素直に配置先へ直接書くと、回線が切れた瞬間の半端なファイルを、次に読むプロセスが掴みます。設定ファイルなら構文エラーで起動しなくなります。

形は決まっています。別の場所に落としきってから、最後に一手で入れ替える。

  1. 一時ディレクトリに取得する
  2. 大きさとハッシュを検証する
  3. 通ってから、所定の場所へ rename で移す

3が要点で、同じファイルシステム内の rename は POSIX で不可分と決められています。古いものか新しいものかのどちらかしか見えず、中間状態が存在しません。ファイルシステムを跨ぐと rename 自体が失敗し(EXDEV)、mv はコピーと削除に切り替わって不可分ではなくなるので、一時ディレクトリは配置先と同じファイルシステムに置きます。

ファイルが複数あるときは、世代ディレクトリごと落としてから、新しいシンボリックリンクを別名で作って rename で被せます。こうすると「7個のうち3個だけ新しい」という状態を作らずに済みます。reload をかけるのはこの入れ替えの後です。順番を逆にすると、検証を通した意味がなくなります。

配置先へ直接書く場合と、一時ディレクトリを経由する場合の比較。直接書くと、取得しながら config.yml に書いている途中で回線が切れ、半端なファイルが配置先に残り、次に読むプロセスがそれを掴んで構文エラーで起動しない。一時ディレクトリを経由する場合は、1 配置先と同じファイルシステムに置いた一時ディレクトリに取得する、2 大きさとハッシュを検証する、3 通ってから rename で所定の場所へ移す。同じファイルシステム内の rename は POSIX で不可分なので、見えるのは古いものか新しいもののどちらかだけで、reload はこの入れ替えの後にかける。ファイルシステムを跨ぐと rename は EXDEV で失敗し、mv はコピーと削除に切り替わって不可分ではなくなる。ファイルが複数あるときは世代ディレクトリごと落とし、新しいシンボリックリンクを別名で作って rename で被せる

帯域: 固め方で決まる

zip や tar で固めて置くと、1バイト変わっても全部落とすことになります。設定ファイル数十 KB なら気になりませんが、100MB のアーカイブを1000台が取りに来ると効いてきます。

軽くする方向は2つです。ファイル単位で置いてハッシュで差分を判定するか、アーカイブごと版を切って変わったときだけ落とすか。前者は取得ロジックが複雑になり、後者は単純な代わりに転送量が減りません。単純さと転送量の交換になります。

固め方で、1回の更新に動くバイト数が決まる。zip や tar で固めて置くと、1バイト変わっても全部落とす。設定ファイル数十 KB なら気にならないが、100 MB のアーカイブを1000台が取りに来ると、掛け算して1回の更新で 100 GB になる。軽くする方向は2つ。ファイル単位で置いてハッシュで差分を判定すると、変わったファイルだけ落とせるが、取得ロジックが複雑になる。アーカイブごと版を切ると、単純で変わったときだけ落とせるが、変わるたびに全部なので転送量は減らない。単純さと転送量の交換

改ざん耐性: 置き場所が取られたら終わる

この方式でもっとも注意が要るところです。

機械は「その URL から落ちてきたもの」を信じて展開するので、ストレージの書き込み権限を取られると、それだけで全機械に任意のコードを配れます。HTTPS は通信経路を守ってくれますが、置かれた中身が正しいことまでは保証しないので、経路を暗号化しただけでは何も解決していません。

ここで署名が要ります。配る側が署名し、機械が公開鍵で検証してから展開します。そうすると、ストレージが取られても署名鍵が無ければ配れません。経路を信じる型から、中身を信じる型への切り替えです。

パッケージ型はこれを最初から仕組みとして持っていて、次の章のdeb 配布: 200台の状態を数えるで扱います。5方式のどこに穴が残るかはコード署名と鍵の置き場所: 5方式に共通する穴でまとめて整理します。

置き場所を誰が読めるか(7軸の外)

改ざん耐性は書き込み側の話でした。読み取り側にも同じだけの判断が要ります。

「HTTP で読めればどこでも構いません」と書きましたが、これは誰でも読めていいという意味ではありません。配るのが設定ファイルなら、そこには接続先のホスト名、内部のエンドポイント、ときには資格情報そのものが入ります。公開バケットに置けば、URL を知っている人は全員読めます。URL は推測されないから安全、という考え方は、台数が増えて URL が構成管理に書かれ、CI のログに出るようになった時点で崩れます。

読める相手を絞るなら、機械側に読み取り用の資格情報を置くことになります。ここでpush 型の章と同じ形が出てきます。push 型では VPN に一時参加するための資格情報が新しい鍵になりました。pull 型では、それが機械の上に置きっぱなしになる鍵に変わります。

その鍵をどう配るかで、性格が2つに割れます。

配り方漏れたとき台数が増えたとき
全機械で同じ鍵1台抜かれると全機械ぶん漏れます配るのは楽です
機械ごとに別の鍵抜かれた1台だけ止められます発行と失効の仕組みが要ります

数台なら前者で足ります。数十を超えると、1台の紛失で全機械の鍵を配り直すことになるので、後者へ寄せる圧力がかかります。ここでも常駐コストの軸が効いてきます。鍵を発行して失効させる仕組みは、動かし続けるものがまた1つ増えるということです。

有効期限付きの URL を配る手もあります。機械に長期の鍵を置かずに済みますが、期限が切れる前に新しい URL を取りに行く経路が別に要るので、最初の資格情報をどこに置くかという同じ問題が、1段ずれた場所に残ります。

読める相手を絞ると、機械の上に置きっぱなしの鍵が1本増える。設定ファイルには接続先のホスト名や内部のエンドポイントが入り、公開バケットなら URL を知っている人は全員読める。push 型の新しい鍵は VPN に一時参加するための資格情報で job の間だけ使うのに対し、pull 型の新しい鍵は読み取り用の資格情報で機械の上に置きっぱなしになる。全機械で同じ鍵にすると、1台抜かれると全機械ぶん漏れるが、配るのは楽で数台ならこれで足りる。機械ごとに別の鍵にすると、抜かれた1台だけ止められるが、発行と失効の仕組みが要り常駐物が1つ増える。有効期限付きの URL を配る場合は長期の鍵を置かずに済むが、期限前に新しい URL を取りに行く経路が別に要り、最初の資格情報をどこに置くかという問題が1段ずれた場所に残る

観測性: 置いた側から見えなくなる

配る側は、置いた時点で仕事が終わります。その後、機械が取りに来たかどうかを知りません。

機械が電源断していても、ネットワークが切れていても、取得に失敗していても、配る側から見た景色は成功と同じです。push 型なら SSH が繋がらない時点で CI が赤くなるのに対して、こちらは失敗を知らせる主体がどこにも居ないので、何も起きないまま時間が過ぎます。

なので、取得結果を機械側から報告させる経路を別に作ることになります。バージョンを添えた死活報告を定期的に投げさせて、一定時間報告が来ない機械を検出します。ここまで作って、ようやく push 型の観測性に並びます。

「配ったつもり」がもっとも起きやすい方式である、と覚えておくのが安全です。

push 型と pull 型の対比。push は配る側から機械へ矢印が向き、NATの内側に入れない代わりにroot権限は数秒だけで失敗はCIが赤くなる。pull は機械から置き場所へ矢印が向き、どこにでも届く代わりに常駐プログラムが権限を持ち続け、失敗が配る側から見えない

7軸で見ると

軸評価push 型との差
到達性◎逆転。どこに居ても届きます
常駐コスト△逆転。エージェントが増えます
権限の粒度△同じ絞り方でも、常時持つぶん危険度が上がります
ロールバック○改善。機械が自分で戻せます
帯域○固め方次第
改ざん耐性△署名を足せば◎になります。足さないと置き場所が単一障害点
観測性△逆転。報告経路を別に作って、ようやく並びます

push 型との比較で反転する軸は、到達性・常駐コスト・観測性の3つです。ロールバックの改善と権限の悪化も向きを変えたことから来ていますが、得たものと失ったものの大きい部分は、この3つに出ています。

次の章では、配るものを「ファイルの集まり」から「パッケージ」に変えます。すると、他の4方式には無い性質が1つ手に入ります。

参考