🪟

[OSS] WSL ContainersでDocker Compose / dipっぽい開発体験を作るCLI「wip」を作った

に公開

普段、コンテナを使った開発ではこんな感じで操作することが多いと思います。

docker compose up
docker compose run web bash
dip rails c

MacやLinuxだと、このあたりの開発体験はかなり整っています。

もともとはWindowsを使っていて、その後しばらくMacに移り、最近またWindowsに戻ってきました。

Macも長く使っていましたが、結局Windowsのほうが自分には合っていました。

理由はかなり生活寄りです。

ゲームもするし、Macのキーマップは最後までいまいち手に馴染まなかった。

開発以外も含めて毎日使うものなので、Windowsのほうが自然に使える感覚があります。

なので、開発環境だけMacに寄せるより、

Windowsのまま快適に開発したい。

そのためにWSL2を使っています。

最近はMicrosoftから、WSL上でコンテナを扱う WSL Containers(WSLC) も出てきました。

Docker DesktopなしでWindows上にコンテナ環境を作れるので、個人的にはかなり面白いです (DockerHubの有料版は、他のクラウドのRegistryより劇的に安いのでありがたく課金させてもらってます!)

ただ、実際の開発で使ってみると、

「Docker Composeやdipみたいないつもの操作感が欲しい」

となりました。

そこで作っているのが wip です。

https://github.com/slidict/wip

wipとは

wipはMicrosoft WSL Containers向けのCLIです。

プロジェクトごとの、

  • コンテナ
  • イメージ
  • 環境変数
  • volume
  • 開発用コマンド

などをwip.ymlにまとめて、

wip up
wip build
wip rails console

のように操作できます。

内部ではWSLCのwslc.exeを呼び出しています。

つまり、wip自身がコンテナランタイムを持っているわけではありません。

WSLCを普段のアプリケーション開発で扱いやすくするためのCLIです。

なぜ作ったのか

WSLC本体でもCompose相当の機能をどうするか、という話は出ています。

ただ、自分としては将来の実装を待つより、今すぐWSLCを普段の開発で使いたかった
そして、Docker Desktopなければもしかしたらメモリを有効活用できるかもなんてのも思いました。

Railsの開発では、

bin/rails console
bin/rails db:migrate
bundle exec rspec

のようなコマンドを何度も実行します。

Docker Composeなら、

docker compose exec app bin/rails console

dipなら、

dip rails c

と書けます。

WSLCでも、今この操作感が欲しい。

それで作り始めたのがwipです。

最初は単純なラッパーでしたが、実際に使っていくうちに、bind mountやWindows / WSL間のパス、build contextなど、WSLC特有の問題にも当たるようになりました。

結果として、単にコマンドを短くするだけではなく、WSLCを普段の開発で使うための差分を吸収するCLIになってきています。

wip.yml

wipではプロジェクトルートにwip.ymlを置きます。

例えばこんな感じです。

version: 1
mode: container

container: app

dependencies:
  app:
    image: myapp:development
    workdir: /app
    ports:
      - "3000:3000"
    volumes:
      - ".:/app"

  redis:
    image: redis:latest

interaction:
  rails:
    type: exec
    command: bin/rails
    container: app
    interactive: true

  rspec:
    command: bundle exec rspec

  shell:
    command: bash
    interactive: true

こうしておけば、

wip rails console
wip rspec
wip shell

のように実行できます。

プロジェクト固有の長いコマンドを毎回覚えなくて済みます。

Composeを使うプロジェクトにも対応している

wipには現在、主に3つのモードがあります。

container
compose-native
compose

containerは、wipがWSLCのコンテナを直接管理するモードです。

compose-nativeは既存のcompose.ymlをwip自身が読み込み、WSLC向けに実行するモードです。

composeは外部のWSLC向けComposeツールと組み合わせるためのモードです。

既存プロジェクトにはすでにcompose.ymlがあるケースも多いので、それを捨てずに使えるようにしています。

wip initcompose.ymlを見つければ、自動的にcompose-nativeを選びます。

WSLCを実際に使うと細かいところでハマる

wipを作っている理由は、単にコマンドを短くしたかったからだけではありません。

WSLCを普段の開発で使っていると、Docker Desktopではあまり意識しなかったところで、ちょこちょこハマります。

そのあたりをwip側で吸収していくのも、作っていて面白いところです。

bind mountが遅い

かなり気になったのが、ソースコードをそのままbind mountしたときの速度でした。

Railsのようにファイル数が多いプロジェクトだと、アプリケーションの起動やファイルアクセスが重くなることがあります。

そこでwipには、ソースコードを直接bind mountする代わりに、WSLC側のnamed volumeへ同期する仕組みを入れました。

sync:
  exclude:
    - .git
    - tmp/
    - node_modules/

同期は、

wip sync

でできます。

監視しながら同期することもできます。(これはwip upできればよかったかなと・・・思ったり)

wip sync --watch

コンテナから見ると普通のLinux側volumeなので、Windows側の大量のファイルを毎回bind mount越しに読む必要がありません。

Docker Desktopを使っていたときにはあまり考えなかった部分ですが、WSLCを実際の開発環境として使おうとすると、このあたりの違いがかなり効いてきます。

WindowsとWSLではパスの世界が違う

もう一つ面白かったのがパスです。

例えばWSL上で、

~/codes/myapp

にいるとします。

Windows側から見ると、

\\wsl.localhost\Ubuntu\home\user\codes\myapp

のようなUNCパスになります。

一方でwslc.exeはWindows側で動いています。

ここを雑に、

/home/user/codes/myapp

として渡すとうまくいきません。

さらに厄介なのが、存在しないパスを渡しても必ずしもエラーにならず、空のディレクトリがmountされるケースがあることです。

コンテナは起動する。

でも中を見るとソースコードがない。

最初はかなり分かりにくいです。

現在のwipでは、こうしたケースを黙って通さず、WSL filesystem上のsync.sourceについては安全側に倒して拒否するようにしています。

WSLC自体がまだ新しいので、このあたりは実機で挙動を確認しながら追従しています。

build contextもそのまま渡さない

ファイルシステムの境界はbuild時にも効いてきます。

そのためwipでは、build contextをそのままWSLCへ渡すのではなく、一度Windows側のローカルキャッシュへstageしてからbuildする仕組みにしています。

.dockerignoreも読み取り、不要なファイルはコピーしません。

地味ですが、こういう部分をCLI側で吸収できると、毎日の開発ではかなり楽になります。

wipはWSLCの「糊付け」

なのでwipは、

wip up
wip rails console

と短く書くためだけのCLIというより、

WSLCを普通のアプリケーション開発で使ったときに出てくる差分を、なるべくwip側で吸収する

という役割も持たせています。

WSLC自体の機能はシンプルで好きなのですが、そのまま日々の開発に使うには、まだ少しだけ糊付けが欲しい。

その糊付けをwipでやっています。

RubyからC# Native AOTへ移行した

wipは最初、Ruby Gemとして作っていました。

自分がRubyを普段使っていることもあって、最初に作るにはかなり楽でした。

ただ、Windows向けの開発ツールとして考えると、

「wipを使うためにRubyを入れてください」

というのは少し違うなと思い始めました。

そこでv2からC#に書き直しました。

現在は.NET Native AOTでビルドしています。

配布物は基本的に、

wip.exe

だけです。

Rubyも.NET Runtimeも必要ありません。

Windows向けCLIとしてかなり自然な形になりました。

起動もRuby版よりかなり軽く感じます。

PowerShellからもWSLからも使える

wipはWindowsの実行ファイルですが、PowerShellだけでなくWSL側からも実行できます。

PowerShellなら、

wip up

WSLなら、

wip.exe up

です。

WSL側にshimを置けば、

wip up

として使うこともできます。

個人的にはWindows TerminalからWSLに入り、そのままプロジェクトを操作することが多いので、両方から同じツールを使えるのはかなり便利です。

doctorも作った

WSL周りは、

  • WSLが入っているか
  • WSLCが見つかるか
  • 設定が正しいか
  • CPU architectureが合っているか

など、環境依存の問題も多いです。

そのため、

wip doctor

も用意しています。

問題があると、

[OK]
[WARN]
[FAIL]

のように確認できます。

自分自身がハマったものを、そのまま診断項目として追加している感じです。

インストール

現在はGitHub ReleasesからWindows x64向けバイナリを取得できます。

https://github.com/slidict/wip/releases

ZIPを展開し、wip.exeのあるディレクトリをPATHに追加します。

確認は、

wip version

です。

WinGetへの登録も進めています。
Scoopは・・・、StarとFork募集中です!

まだ発展途上

WSL Containers自体がまだ新しい仕組みです。

そのためwipも、それに追従しながら開発しています。

特に、

  • bind mount
  • Windows / WSL間のfilesystem
  • Compose互換
  • interactive TTY
  • Dev Containersとの組み合わせ

あたりは、まだ検証を続けています。

Docker DesktopやDocker Composeを完全に置き換えることを目的にしているわけではありません。

本家側が便利になれば、wip側でやらなくてよくなることも出てくると思います。

それでも今は、WSLCを今すぐ実際の開発で使うためのCLIとして作っています。

Windowsの開発環境も結構面白い

開発環境の話をすると、どうしてもMacやLinux前提の話が多くなります。

僕も長い間Macを使っていました。

ただ、Windowsに戻ってきてWSL2を使い始めると、

Windows

WSL2

WSL Containers

という構成もかなり面白いです。

Linux環境を使いつつ、WindowsネイティブのCLIもそのまま使える。

まだ荒削りなところもありますが、そのぶん触っていて面白い部分も多いです。

本家の機能が揃うまで待つのではなく、今あるWSLCを使って、足りないところは自分で埋める。

wipはそんな感じで作っています。

WSL Containersを触っている方がいれば、ぜひ試してみてください。

GitHub:

https://github.com/slidict/wip

Discussion