Skip to content

Latest commit

 

History

History
279 lines (214 loc) · 18.8 KB

File metadata and controls

279 lines (214 loc) · 18.8 KB

Optional network bridge

Latticeのproject dashboardは既定でloopbackだけにbindし、bridge用socketや設定を作らない。 LAN上のreverse proxyなどから閲覧する時だけ、利用者がlisten IPを明示してbridgeを有効化する。 postinstallでは質問やnetwork公開を行わない。

工程操作から配信漏れを知る

lattice status --jsonとToDoの保存操作が、工程結果のstdoutに加え、stderrへ lattice.dashboard_delivery.v1を返す。工程の保存成功を公開配信の成功と読み替えない。

state 確認できたこと
local_only PC内の工程表が利用可能。reasonbridge_unconfiguredまたはbridge_disabled
bridge_only bridge設定を利用。hubが未設定のため、その先の公開配信は未確認
hub_delivered hubが対象工程を受理し、一覧にonlineで掲載し、工程HTMLの中継が成功
failed 配信を確認できない。reasonnext_actionで原因と復旧入口を返す

有効なbridge設定がある端末では、工程操作が常駐設定・実行体・実processを確認し、欠落や実行pathのずれを 正規のbridge reconfigureで復旧してから配信を確認する。無効・未設定のbridgeは自動公開しない。 PCの初期化等で設定自体が失われた時はlocal_onlyがその事実を示す。公開先を推測して復元はしない。

明示の復旧入口lattice todo dashboard ensure --jsonは、ローカルdaemonから設定済みbridgeとhub配信までを 処理する。結果はlattice.todo_dashboard_ensure_result.v1で、activity.deliveryに同じ配信結果を含む。 配信失敗では非0を返す。通常の工程保存は配信故障で止めず、保存結果と配信失敗を別々に返す。

bridge setupreconfigureにhubが設定されている場合は、dashboard起動、hubへの登録、一覧と工程HTMLの 確認までを成功条件とする。stdoutのlattice.bridge_cli_result.v4は維持し、stderrへ確認結果を返す。通信や中継が 失敗した時はBRIDGE_HUB_DELIVERY_UNCONFIRMEDで非0を返す。通信故障でも保存済みの接続設定は保持する。 hub URLは端末から直接到達する登録先を使う。reverse proxy経由の登録は接続元がproxyになり、hubから端末への 経路を誤る。別の公開HTTPS入口がある構成では、公開HTTPSとSSEの受入も本書後半に従って実施する。

配信確認は全OS共通とし、常駐の復旧はMacのLaunchAgent、WindowsのStartup Folderの既存入口を使う。 実行中の版だけが異なる場合は既存の自動更新に任せ、配信確認から再設定を強制しない。

共通の設定入口

TTYではlattice bridge setupで安全側既定の対話wizardを開始できる。最初の公開確認は既定で無効を選び、 cancelした場合は設定を変更しない。非TTYではhangせず、次の非対話commandを案内する。 portを省略するかautoにすると、49152–65535から候補を重複なく選び、 実際のexclusive bindとhealth確認に成功したportだけを保存する。

lattice bridge setup --listen 192.168.1.50 --port auto --dashboard --allow-host lattice.example.com --json

--dashboardは現在のlocal dashboard descriptorをrequestごとに解決するため、dashboard再起動でportが変わっても bridge設定はstaleにならない。固定upstreamを使う場合だけ--upstream http://127.0.0.1:4318を指定する。 listen IPは常に許可Hostへ入り、reverse proxyで公開するhostnameは--allow-hostを反復して追加する。 許可されていないHostは421となるため、DNS rebinding originへ工程情報を返さない。

lattice bridge status --json
lattice bridge reconfigure --listen 192.168.1.50 --port auto --dashboard --json
lattice bridge disable --json

設定は~/.lattice/bridge.jsonへmode 0600でatomic保存する。setupreconfigureは実bridge daemonが 選択socketをexclusive bindしhealthを返すまで成功にしない。起動に失敗した場合は旧設定へ戻す。 disableもbridge socketの停止確認後に成功し、loopbackのlocal dashboardは停止しない。 configまたはdaemon descriptorが壊れている場合、disableは公開socketのfail-closed停止を優先して破損control fileを除去し、JSON結果のrecoveryへ処置を明示する。その後はsetupで再設定できる。

自動化・隔離testではabsoluteなLATTICE_CONFIG_DIRで設定rootを変更できる。無効な設定、低いport、 使用中の明示port、危険なrequest target、到達不能upstreamはsilent fallbackせずtyped errorを返す。

常駐が黙って死んでいないか確かめる

reachableは「設定したaddressで誰かが応答しているか」しか答えない。常駐設定(macOSのLaunchAgent、 WindowsのStartup launcher)が消えたbinaryを指していると、supervisorは起動できないprocessを回し続け、 どこにもエラーが出ないまま公開面から端末だけが消える。statusはそれを1回で名指しする。

lattice bridge status --json

bridgeが無効な間は以下すべてnullで、bridgeを有効にしている時だけ観測する。

persistence — 常駐設定が実際に起動する対象。

field 意味
state installednot_installedunreadable
loaded launchdへ読み込み済みか。Windowsには対応概念が無いのでnull
node_pathnode_exists 起動するNode実行体と、それが今も存在するか
bridge_pathbridge_exists 起動するbridge scriptと、それが今も存在するか
error unreadableのときだけtyped code(例BRIDGE_LAUNCH_AGENT_PLIST_UNSAFE
error=BRIDGE_PERSISTENCE_STATE_SPLIT launcherまたはplistの片割れだけが残った状態。手でfileを消さず、lattice bridge reconfigure --jsonで常駐設定を揃える(0.57.2以降)

BRIDGE_PERSISTENCE_STATE_SPLITは、常駐設定の一方(macOSのplist、またはWindowsのlauncher/descriptor)だけが 存在することを示す。これは復旧対象そのものが壊れている状態なので、手作業でfileを削除せず、 lattice bridge reconfigure --jsonを実行して正規のinstall経路で両方を再生成する。

runtime — いま応答しているprocess自身の申告。staterunningnot_runningunattesteddescriptor_invalidで、running以外では各値がnullになる。runningでも、identityを返さない 0.55.0より前のdaemonが走っている間はversion以下がnullになる(この場合runtime_driftは空になり、 乖離の有無は判定できていない——「乖離なし」ではない)。

reconfigure直後は、identityの確認requestが400msで打ち切られるため一時的にunattestedを返すことが ある(daemonの起動直後と競合する)。数秒おいて引き直せばrunningになる。続くようなら本物の不整合で、 reachableがtrueでも公開面は認証できていない。

field 意味
pid 応答しているprocessのpid
version そのprocessが読み込んでいるLatticeの版
node_pathnode_version そのprocessを実行しているNodeの実体pathと版
bridge_path そのprocessが実行しているbridge script

runtime_drift — 両者の食い違い。空配列は「差が無い」または「runtimeが名乗っていないので 判定できない」のどちらかである。

意味
bridge_path 常駐設定と違うtreeのcodeが走っている(開発treeの残骸など)
node_path 常駐設定が指すnodeと実走nodeが別実体。焼くのは意図的にaliasなので、比較はrealpathで行う
version npm更新後まだ旧moduleを保持している

remedy — 自己解消しない状態にだけ、打つべきコマンドが入る。出るのは次の4つ。

  • persistence.node_existsまたはbridge_existsがfalse(起動対象が消えた)
  • persistence.statenot_installed(bridgeは有効なのに常駐設定が無い。いま走っているdaemonが 最後の1つで、再起動しても戻らない)
  • persistence.stateunreadable(常駐設定を読めない)
  • runtime_driftnode_pathまたはbridge_pathがある

versionだけの差にはremedyを出さない。daemonは60秒ごとにon-diskのpackage.jsonと自分の版を 突き合わせ、差があれば自ら終了してsupervisorに新codeで起動し直させる。放っておいて最大1分ほどで 解消するので、コマンドを出す状態ではない。自己解消する差にコマンドを出すと、本物の障害が埋もれる。

remedyが出たらreconfigureで作り直す。plistやlauncherを手で書き換えない。

lattice bridge reconfigure --json

macOSのLaunchAgent載せ直しでは、launchctl bootoutのexit 0はunload受付だけである。labelが domainから消える(launchctl printが113)前にbootstrapすると、launchdは 5 Input/output errorを返す。reconfigureはprint 113を待ってから載せる(0.60.7・ ADR 0179)。失敗時は launchctlのexit codeとstderrがlattice.cli_error.v2detailに残る。

公開面から自分のprojectが消えた時(last_heartbeat

hubへ繋いでいる端末では、runtime.last_heartbeatが最後にhubへ名乗った結果を持つ。公開一覧で 自分のprojectがofflineになっている時、原因が端末側か配線側かはここで分かれる。

state 意味 打つ手
accepted 全件受理された。公開面に出ていないならhub側の可視性設定を見る
partial 一部が他の生きた端末に所有されている。rejected_projectsが名指しする 意図した端末なら放置。奪うならadopt
rejected hubがrequest全体を拒否した。detailにtyped code detailのcodeで分岐
unreachable hubへ届かない。配線かhubの停止 hubのURLと生死を確認
skipped_no_projects 配信しているprojectが0件。名乗るものが無い 正常な静止。公開したいならそのrepoで作業する
skipped_no_dashboard dashboard daemonを観測できない。配信そのものが立っていない daemonの生死を見る。todo系commandを1回打てば起動する
null hub未設定、またはまだ1回も送っていない

名乗る集合はdaemonが実際に配信している集合そのもの(ADR 0165)。配信集合は last_seen_atが1週間以内、またはactive run、または監査待ちPhaseがあるprojectである。 人がCLIを叩かなくても、その条件を満たす限り公開面に残る。1週間を超え、runも監査待ちも 無いprojectは配信から外れる。heartbeatの90秒TTLは、配信そのものが止まった時 (daemon停止)にofflineへ落とすためのもので、鮮度窓ではない。

焼き込むnode pathの選び方(ADR 0163)

常駐設定へ焼くnodeのpathは、版付きの実体(homebrewのCellar/node/<version>/bin/node、nvm-windowsの 版ディレクトリ)ではなく、同じbinaryを指すとrealpathで検証できた安定alias/opt/homebrew/bin/nodeC:\Program Files\nodejs\node.exe)を選ぶ。brew upgrade nodeが旧versionのディレクトリごと消しても 起動対象が残るようにするためである。

安定aliasを検証できない環境(shim方式のasdf/volta等。shimは自身のlauncherへ解決されるので実体と 一致しない)では、版付きpathのまま焼く。検証していないpathを推測で焼けば別のnodeでdaemonが起動して しまうためで、そこでの防御は起動継続ではなくnode_existsによる消滅の可視化である。

0.55.0より前に設定した常駐は自動では移行しない。 焼き直しはreconfigureを実行した時にだけ 起きるので、Latticeを更新しただけの端末は版付きpathを抱えたままになる。更新後に各端末で lattice bridge reconfigure --jsonを1回打つ。現在どちらを焼いているかはpersistence.node_pathで読める。

なおsetupreconfigureをnode_modules配下でない実体(開発tree)から実行すると、結果のwarningsBRIDGE_PERSISTED_FROM_DEVELOPMENT_TREEが入る(該当しなければ空配列)。そのtreeを動かすと常駐が 止まり、npm更新も反映されない。開発treeから常駐させること自体は正当な操作なので拒否はしない。

listen IPがDHCPで動く場合

設定したlisten IPがホストから消えると、古いsocketは死んだアドレスへ取り残され、LANから到達できなくなる。 daemonは各reconcileで実効アドレスを解決し直し、同一subnet(IPv4 /24、IPv6 /64)に生きたアドレスがあれば そこへbindし直す。VPNや別NICなど異なるnetworkのアドレスは採用せず、候補が無ければ BRIDGE_LISTEN_ADDRESS_ABSENTで公開socketをfail-closedにする。再bind先は許可Hostへ自動で加わる。

LATTICE_BRIDGE_REGISTRAR_SSH_HOSTLATTICE_BRIDGE_REGISTRAR_SCRIPTを両方設定すると、新しいbindingを 張るたびにssh <host> <script> <port>でreverse proxy hostへ自己登録する。アドレスは送らず、remote側が ssh送信元から決めるため、各hostは自分自身しか登録できない。登録の失敗はbridgeを落とさずstderrへ typedに報告する。この配線が無いと、Caddy等が持つリテラルはlease変更のたびに黙って陳腐化する。

reverse proxyへ逆トンネルで繋ぐ(LAN bindを使わない)

reverse proxy hostへsshで到達できるなら、LANへbindせずloopbackだけで公開できる。bridgeが動くhostから 接続しに行くため、そのhostのLAN addressはreverse proxyのどこにも現れず、追従も自己登録も不要になる。

lattice bridge setup --listen 127.0.0.1 --port 53939 --dashboard --allow-host lattice.example.com --json
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \
  -R 172.18.0.1:53939:127.0.0.1:53939 proxy-host

reverse proxyはこの固定endpointだけを見る。転送口のbind先は、reverse proxyが到達できるaddressにする。 Docker上のreverse proxyでは、containerの127.0.0.1はcontainer自身のloopbackでありhostのそれではないため、 hostのloopbackへ開いた口には届かない。対象networkのgateway(docker network inspectGateway)へbindする。

sshdは既定のGatewayPorts noだと127.0.0.1にしかbindできない。clientspecifiedにすると、clientが明示した addressだけにbindする(yesと違い全interfaceへは晒さない)。host firewallがINPUTをDROPしている場合は、 その1 portだけを許可する。ExitOnForwardFailure=yesは、転送口を開けないまま接続だけ生かす状態を防ぐ。 常駐はprocess supervisorのKeepAliveに任せ、切断時は張り直す。

Docker Caddy/Cloudflare Tunnelへ接続する

bridgeを有効化したMacとreverse proxy hostの間で、まず許可Hostを付けたLAN到達を確認する。 この段階が失敗している時はDNSやTunnelを追加しない。

curl --fail --header 'Host: lattice.example.com' \
  http://MAC_LAN_IP:BRIDGE_PORT/projects/

Caddyは既存のDocker networkと証明書運用を維持し、Lattice用siteだけを追加する。

lattice.example.com {
	reverse_proxy MAC_LAN_IP:BRIDGE_PORT {
		flush_interval -1
	}
}

本番反映はcontainer内でcaddy validateを通してからcaddy reloadする。Caddyfileを単一ファイルで bind mountしている構成では、atomic renameでhost側fileを置換するとcontainerが旧inodeを参照し続ける。 更新前backupを残し、inodeを維持するin-place更新を使うか、directory bind mountへ変更する。

remote-managed Cloudflare Tunnelでは、Tunnel実行tokenを設定APIの代用にしない。Cloudflareの正規管理面で public hostname lattice.kitepon.dev を次のoriginへ対応付ける。

  • Service: https://caddy:443
  • TLS Origin Server Name: lattice.kitepon.dev(管理面に同等の設定がある場合はMatch SNI to Hostでもよい)

http://caddy:80は選ばない。CaddyのHTTPからHTTPSへのredirectをTunnelがorigin応答として返す構成は、 外部requestが同じ公開URLへ戻るredirect loopになり得るためである。外部gateではredirectを追って200にせず、 最初の応答がHTTPSの200であることを確認する。

受入は次の3 gateを独立して記録し、後段の成功で前段を代用しない。

  1. LAN bridge: reverse proxy hostから許可Host付きでhttp://MAC_LAN_IP:BRIDGE_PORT/projects/が200。
  2. Docker Caddy: CaddyへHost: lattice.kitepon.devを付けたHTTPS requestが200。証明書検証を省略する 内部probeを外部公開成功の証拠にはしない。
  3. Cloudflare public HTTPS: https://lattice.kitepon.dev/projects/がredirectなしで200となり、一覧から開いた /projects/<project_id>/のHTML titleがLattice — <project名> 依存工程図である。

公開viewerの404も、ブラウザとAPIの両契約を別々に確認する。未知URLへAccept: text/htmlを 付けたrequestはHTTP 404かつContent-Type: text/htmlで、noindex, nofollow/projects/https://kitepon.dev/への戻り先を持つ。Accept: application/jsonでは HTTP 404かつContent-Type: application/jsonで、既存の lattice.todo_gantt_http_error.v1を返す。

curl --silent --show-error --include --header 'Accept: text/html' \
  https://lattice.kitepon.dev/unknown
curl --silent --show-error --include --header 'Accept: application/json' \
  https://lattice.kitepon.dev/unknown

外部gateはHTMLだけで閉じず、各projectの https://lattice.kitepon.dev/projects/<project_id>/eventsも確認する。応答は200かつ Content-Type: text/event-streamで、接続直後にevent: stateと現在のhead_digestを返さなければならない。 接続を開いたまま正規のTodo更新を行い、新しいstateが同じstreamへ届くことを確認する。切断後に再接続しても 再び初回stateが届き、そのdigestが最新headと一致することまでを継続・再接続gateとする。

curl --fail --show-error --include --no-buffer --max-time 15 \
  https://lattice.kitepon.dev/projects/PROJECT_ID/events