叩いてみるとすぐ、`wip rails c`のような対話コマンドが標準入力をそのままパイプで渡していて、`rails c`のプロンプトがまともに効かないことに気づいた(実TTYが割り当たっていない)。この場では原因のあたりだけつけて、直すのは日付が変わってからに持ち越している。1日目は「`dip`でできていたことを、動く形でとにかく一通り揃える」だけで終わった感覚だった。
== 2日目(8/1): dip風の機能を固めた1日
日付が変わって0時、`wip up`/`wip down`を足して永続コンテナを管理できるようにしたところ(`629d71c`)からこの日は始まっている。深夜2時台までひと続きで作業して、そこから9時間ほど空け、正午過ぎにもう一度机に向かい、夕方まで続けるという1日だった。
=== v0.1.4 → v0.2.0 — 30秒で訂正したバージョン番号
`wip up`/`wip down`を足した直後、機械的に「patch」でバージョンを刻んで`v0.1.4`のPRを作った。だが33秒後、もう一度ワークフローを回して`v0.2.0`に訂正している。コミットメッセージにそのまま理由が書いてある。
[quote]
---
`wip up`/`down` are new commands, so this should be a minor bump per semver, not a patch.
---
新しいコマンドを追加したのだから、semver的にはminorが正しい——という当たり前の判断を、リリースした直後に自分で拾って直した格好になる。1分と経たないうちに`AGENTS.md`にバージョン方針(`feat`はminor、`fix`はpatch、breaking changeはmajor)を明文化していて(`66b0124`)、二度と迷わないようにする流れが早い。
=== v0.3.0 — 長時間起動するサーバーに対応
`wip up`はそれまでイメージの既定の`CMD`を引数なしで起動していたが、`compose.yml`の`command: "local"`のように`$1`で分岐するエントリーポイントだと何もせず即終了してしまうケースがあった。`up.command`を`wip.yml`に追加して、`wip up`が渡すコマンドを明示できるようにした(`491ab19`)。
=== v0.3.1 — TTYとノイズの後始末
前夜メモしておいた`rails c`のTTYの件をここで直す(`e78d6e4`)。`wip rails c`を叩き直すと、ちゃんとプロンプトが効くようになっていた。あわせて、コンテナがまだ無い状態で`wip up`した時に余計な「見つかりません」ノイズが出ていたのも静かにした(`17fce42`)。READMEを英語化してOSSとしての体裁も整えている(`37dcfec`)。
=== v0.4.0 — 依存コンテナとネットワーク
ここが2日目で一番中身の濃いところ。TTYを直したはずの`wip rails c`が、自分が普段使っているRailsアプリ(slidict.io)でまた動かないと報告が上がってきた。原因を辿ると、TTYとは全く別の話だった——`wip`はまだ単一のコンテナしか管理できず、`wip up`は`config/database.yml`が要求する`development.mysql`のようなサービスを一切起動していなかった。`bin/rails c`は存在しないホスト名を延々と名前解決しようとして固まっていただけだった。
`compose.yml`ならブリッジネットワーク+サービス名で当たり前に解決できる話が、`wip.yml`には仕組みごと無かった、というのが実態。そこで`defaults.network`と`dependencies`マップを追加し、`wip up`がまずネットワークを作り、`dependencies`を名前ごとに起動してから本体コンテナを同じネットワークに繋ぐようにした(`e18f816`)。
## [source,yaml]
defaults:
network: app-tier
dependencies:
redis:
image: redis:latest
development.mysql:
image: mysql:8.0
----------------
## [source,bash]
wip up -d # ネットワーク作成 → development.mysql/redis → app の順で起動
wip rails c
# > Redis.new(host: "redis").ping #=> "PONG"
# > ActiveRecord::Base.connection.execute("SELECT 1") # development.mysql:3306 に届く
---
実際に手元の`wslc.exe`とslidict.ioに対して、ネットワーク作成からMySQL/Redis/appの起動、名前解決、appからMySQLへの生きたTCP接続まで通しで確認してからマージしている。ここでようやく「`wip.yml`一枚でアプリとサイドカーをまとめて面倒見る」という、`dip`に近い体験になってきた。
=== v0.4.1〜v0.4.3 — Windowsネイティブと`--debug`
Windows上のRuby(WSLの外)から直接`wip`を動かそうとすると壊れる箇所があり、そこを直したのが`v0.4.1`(`030df14`)。
`v0.4.2`では、`wip rails c`が明らかに起動に時間がかかる場面に当たって、何が遅いのか切り分ける手段がないことに気づいた。そこで`--debug`フラグを足して、ステップごとの所要時間とホストのリソース監視(CPU/メモリ/ディスクI/O)を表示できるようにした(`5e3346b`)。
## [source,bash]
wip rails c --debug
# wip: [debug] running: wslc.exe exec -it -w /app app bin/rails c
# + wslc.exe exec -it -w /app app bin/rails c
# wip: [debug] still running (load 0.42 ... | mem 3.1G/15.6G | io read 11800KB/s write 300KB/s | ...)
---
このときは単に「遅いときに原因を切り分けたい」くらいの気持ちで足した機能だったが、CPU/メモリはほぼ暇なのにディスクI/Oだけ動き続けている、という妙な数字がすぐ出た。この時点ではまだ「なんでI/Oだけ動いてるんだ」で終わっていて、これが翌日bind mount問題を突き止める決め手になるとは思っていなかった。`v0.4.3`では、`--debug`の出力に環境変数の値をそのまま出してしまっていたのをマスクし(`64eff80`)、インタラクティブなターミナルではスナップショットの出力先をログファイルに切り替えてディスクI/Oの数値も出すようにしている(`943a269`)。
=== v0.5.0 — `.env`/`.dockerignore`、そして夜の宿題
夕方、`.env`の自動読み込みと`.dockerignore`を尊重したビルドを足した(`616f7a1`)。`docker compose`ではあたり前だった挙動が、これで`wip`でも当たり前になった。
その日の終わりに、ロードマップのセクションをREADMEに新設している(`39d617b`)。中身は「`depends_on`のような起動順序・ヘルスチェック」「`wip logs`でのログ集約」「`wip provision`」「プロファイル」「設定ファイルのマージ」「named volumeのヘルパー」「`--scale`」——つまり`compose.yml`が持っている機能を`wip`自身に一つずつ再実装していく、という野心的な計画だった。そのすぐ後に、bind mount起動が遅い件も同じロードマップに追記している(`92f6890`)——原因はまだ特定できておらず、「次に手をつけるべき課題」として書き残しただけの段階だった。この2つの書き置きが、翌日まったく違う形で決着することになる。
== 3日目(8/2): 方針転換、一番の壁、そして仕上げ
前日夕方の続きから一晩、18時間近く空けて、8/2の10時半、この日は「昨夜書いたロードマップを自分で覆す」ところから始まった。
=== v0.6.0 — Compose再実装をやめて、外部ツールへ橋渡しする
一晩置いて考え直した結果、`depends_on`の順序制御・named volume・プロファイル・スケーリング・ログ集約を`wip`自身に再実装していく方針をやめた。`wslc`にはまだCompose相当の仕組みが無く(https://github.com/microsoft/WSL/issues/40948[microsoft/WSL#40948]で公式に議論・トラッキングされている段階)、その隙間を埋めようとする独立したサードパーティ製ツールがすでにいくつか出てきていた——Pythonのhttps://github.com/bacarndiaye/wslc-compose[bacarndiaye/wslc-compose]、Goのhttps://github.com/inuyume/wslc-compose[inuyume/wslc-compose]など。`wslc`自体がまだ生まれたばかりで仕様も動いている以上、`wip`が全部を自前で再実装するより、すでにCompose相当をやろうとしている外部ツールに任せ、`wip`はその手前の薄いブリッジに徹する方が理にかなうと判断した(`9f2af4a`)。
## [source,yaml]
compose:
service: app
command: wslc-compose # 使っているcompose-for-wslcツールのバイナリ名/パス
file: compose.yml # 省略時はwip.ymlの隣を自動検出
---------------------------------------------
`compose.command`はデフォルトを持たない必須項目にして、特定の実装をwip側で贔屓しないようにした。`wip up`/`wip down`はそのまま`<compose command> up -d`/`down`に委譲し、`wip exec`/カスタムコマンドは`compose.service`の中で実行、`wip run`はこの語彙にephemeralコンテナの概念が無いので警告付きで`exec`にフォールバックする——外部ツールが対応しているコマンド語彙(`-f FILE [-p PROJECT] up|down|exec|logs`)の範囲でだけ動く、と割り切った設計。
同じ勢いで、bind mount起動が遅い件もREADMEに書き足した(`2a1090c`)。ソースを読み取り専用でマウントしつつ、実際にアプリが触るのは名前付きボリュームにして、`rsync`でホスト→ボリュームに一方向ミラーリングする、という回避策をドキュメント化しただけの段階——まだ手順を書いただけで、`wip`自体には何も組み込んでいない。ここまでを`v0.6.0`としてまとめてリリースした。
その直後、`wip up`がcompose modeでTTYを計算し忘れていた不具合と、相対パスの`compose.file`がカレントディレクトリ基準になっていて`wip.yml`の場所によって挙動が変わっていた不具合を直し(`5ff7678`)、`wip doctor`にもcompose設定の解決結果を出すようにしている。
=== v0.7.0 — 一番の壁: bind mountがなぜか遅い
`wslc`のコンテナは独自のVM上で動くため、ホストのアプリディレクトリをそのままbind mount(`.:/app`)すると、virtiofs越しの共有になる。RubyのZeitwerkオートローダーのように起動時に大量のファイルをstat/openするフレームワークだと、そのすべてがVMをまたぐラウンドトリップになり、CPUは暇なのに起動だけが数分単位で固まって見える、という現象に遭遇していた。前日足した`--debug`のリソーススナップショットで、CPU/メモリが暇なのにディスクI/Oだけ動いている状態が見えたことで、「これはbind mountの往復コストだ」と判断できた。
回避策をREADMEに書いてからおよそ1時間半後、手順を書いて終わりでは毎回手動でやる羽目になると思い直し、その日のうちにこれを`sync:`ブロックとして`wip`自体に組み込んだ(`ccfcd71`)——今振り返っても、このプロジェクトで一番大きな節目だったと思う。
## [source,yaml]
sync:
exclude:
- .git
- tmp/
- node_modules/
---------------
## [source,bash]
wip doctor
# [OK] sync: source=. volume=app-src target=/app mount=/host-src
wip up -d
# (起動前に rsync で . を app-src ボリュームへミラーリング)
wip rails c --debug
# + wslc.exe exec -it -w /app app bin/rails c
# 前日と違って"still running"のI/Oが張り付いたログが出ないまま、数秒でプロンプトが返ってくる
---
`wip up`がコンテナ起動前に一度ミラーリングし、`wip sync --watch`でファイル変更を継続的に反映し、`wip doctor`が解決済みのsource/volume/targetを表示してrsync未インストールなら導入ヒントも出す——前日の夜にロードマップへメモした課題を、その日のうちに本丸の機能として組み込むところまで終わらせた形になる。`sync:`は`compose:`と併用できないようにもした——compose modeでは volume のレイアウトはcompose側が持つものだから。
=== v0.7.1〜v0.7.2 — 組み込んだ直後の地ならし
大きな機能を入れた直後は大抵ボロが出る。`sync:`も例外ではなく、同じ日のうちに以下のようなコマンドを叩いて穴が見つかった。
## [source,bash]
wip sync --watch --interval -1
# ArgumentError: sleep(-1) (Rubyの生例外がそのまま出て落ちる)
# Dockerfileにrsyncを入れ忘れたイメージで
wip sync
# exec failed: "rsync": executable file not found in $PATH (wipのヒントが出ない)
# wip.ymlにcommands.syncを書いていたケース
wip sync
# (ビルトインのsyncに黙って奪われ、commands.syncが呼ばれない)
---
`--interval`はconfig読み込み時と同じバリデーションを通すようにし、rsync未検出のヒントは実行ファイル名がどちらの語順で出ても拾うようにし、`commands:`の`sync`は「ビルトインが優先されるので`wip dispatch sync`を使って」と案内するようにした(`fcbfa96`)。同じ`v0.7.1`では一時ファイルの取り扱いもセキュリティ観点で見直し、予測可能なパスや競合状態を突かれないよう強化している(`515d371`)。`v0.7.2`では`--debug`のログファイルが実行完了後に消えてしまい、後から見返せなかった不具合を直した(`be9a08f`)——せっかく`--debug`でディスクI/Oまで記録しても、コマンドが終わった後にファイルが残っていなければ意味がない。
ここまでで午後2時過ぎ、一旦手が止まっている。
=== v0.7.3〜v0.7.4 — 夜、最後の一押し
午後2時過ぎからおよそ6時間半空けて、夜8時半すぎに再開。そこから30分ほどで、この3日間の締めくくりになる修正を立て続けに入れて、21時過ぎに手を止めている。
`wip rails c`のような対話コマンドの出力中にCtrl-Cを押すとクラッシュしていたのを、きれいに終了するよう修正(`5e1ba0f`、`v0.7.3`)。続けて、出力を子プロセスから読み取って転送する`pump`処理の`rescue`範囲が広すぎて、書き込み先(ターミナルやログファイル)の本物の失敗まで握りつぶしていたのを、読み取り側だけに絞る修正を入れた(`a576c77`)。
そして21時過ぎ、`wip sync`で毎回ミラーリングにかかる時間を見ていて、ローカルのホスト→ボリューム間コピーにしては妙に時間がかかることに気づいた。デフォルトのrsyncフラグが`-a`(所有者/権限保持+チェックサムベースの差分転送込み)になっていたためで、`-r -l -t --whole-file`に絞ってこの無駄を削った(`ddfdade`、`v0.7.4`)。
## [source,bash]
wip sync
# rsync -a --delete ... (旧: 毎回チェックサム比較で時間がかかる)
# ↓
# rsync -r -l -t --whole-file --delete ... (新: サイズ+mtimeだけで済ませる)
---
同じユーザーの中でホストとコンテナがボリュームを共有しているだけなので、所有者/権限の保持もネットワーク越しの差分転送も要らない——`sync.options`で必要なプロジェクトだけ元の挙動を足し戻せるようにして、この日の作業を終えている。
派手さはないが、どれも長く使うツールとしての信頼性に直結する部分だった。3日目1日だけで、「前夜の方針を自分で覆す→一番の壁にぶつかって本丸の機能を組み込む→そのバグを塞ぐ→夜にもう一押しして仕上げる」まで終わらせたことになる。
== どう役に立っているか
`sync:`を組み込んでからは、bind mountのまま使っていた頃と比べて`rails c`や`bundle`起動時の「なぜか固まる」感覚がかなり解消された。Docker Desktopを常駐させずに、`wslc` + `wip`だけで普段のRails開発が回るようになってきている。
常駐ソフトを1つ削れたこと自体は地味だけど、マシン全体の見通しが良くなった実感がある。
== 今後
`docker compose`の代替としてはうまく動きそうな手応えがある。まだ`wip provision`(bootstrap一発コマンド)や設定ファイルのマージなど、`dip`/`compose`に対して足りない部分は残っているので、個人開発を通してこれからも改善していくつもり。`wslc`自体もまだ動いている最中のツールで、公式のCompose対応(microsoft/WSL#40948)がどう転ぶか次第で`wip`の立ち位置も変わってくるはずだけど、そこは`compose:`モードで外部ツールに任せる形にしてあるので、`wslc`側や周辺エコシステムの進化にはうまく乗っていきたい。