自作Rubyスクリプトをrubygems、docker imageとして配布、利用するために

問題意識

Ruby で書いた自作のツールを手軽に持ち運んで使いたい。そのために

  • rubygems で配布したい(Ruby 環境を持っている場合向け)
  • Docker image で配布したい(Ruby 環境を持っていない場合向け)

以上二つを同時に満たしたい。

この辺はまぁずっと持ってる課題意識ではあるし、もう一つのアプローチとしては Wasm もある

Wasmで少しだけ手軽にRubyとRubyスクリプトを持ち運ぶ (2024-05-25) | あーありがち

が、今回はあちこちのクラウドでそのまま runtime として扱いやすい Docker の方の話を扱う。

rubygemsを作る部分は割愛

bundle gem して雛形を作って、頑張ってコードを書くべし。

docker build時に注意の必要なこと

「持ち込んだもの」は消せない

どういうことかというと、ポイントは以下。

  • COPY*.gem を「持ち込む」とそのレイヤーが作成される → 消すことができない
  • curl などで build プロセス内で「取りに行った」ファイルはその一連の操作を同一の RUN 内に収めてあればレイヤーを残さずに消すことができる

ここで「大した大きさじゃないので気にしない」という判断ができるなら、置き場所さえ気をつければスルーしてもよさそうではある。

「取りに行く」方法の実現方法と妥協点

「じゃあ(容量云々ではなく存在自体が)気になる場合はどうしたらよいのか?」と気になったので、実現方法を考えてみた。

こんな感じ。今回は rubygems の話なので、rubygems 限定で考えると、

  • 公開されている rubygems は rubygems.org/downloads/<name>-<version>.gem という URL から fetch できる
  • 公開前の local のものも同じルールに従う URL になっていれば開発中のものをコンテナの中から取りに行くのもそれほど難しくなさそう

Host OSにrubygems.orgっぽいURLを作る

結論コードだけ書くとこんな感じ。

require 'webrick'

Process.daemon(true)

File.open('webrick.pid', 'w') { |f|
  server = WEBrick::HTTPServer.new(
    StartCallback: -> {
      f.puts $$
      f.flush
    }
  )
  server.mount('/downloads', WEBrick::HTTPServlet::FileHandler, '<ROOT>/pkg')
  server.start
}

bundle gem で標準的に作られる pkg/ 以下を downloads/ と見せるように webrick でサーバを立てる

だけ。pid ファイルを開いて内容を書き込んでいるのは Process.daemon を使って tty から detach したうえで、build 終了後にストップするため。

Dockerコンテナの中からHost OSのURLを叩くには

add-host オプションで build 中のコンテナ内の /etc/hosts にエントリを追加しておく必要があるらしい。

docker build --add-host host:**host-gateway**

これを与えておくと Dockerfile の中で

curl -O http://host/downloads/<name>-<version>.gem

として目的のファイルをダウンロードしてくることができる。

これで開発中のバージョンの gem ファイルをホスト OS から取得して docker image を build することができる。

ただしCI/CD上でホストを見失う

手元でやっている分には dockerd は localhost で動いており、gateway の IP アドレスも分かっているが、「host-gateway is どこ?」というか、「Host OSはgatewayと同一なの?」な環境で同じことを実現するのは難しい。

今回は

  • 未公開の gem を Docker image に収めて公開したいわけじゃない

ということで、CI/CD 上では rubygems.org を参照できるように切り替えることとした。

※ どこか外部のストレージに置いてそのストレージにアクセスしにいければなんでもよいので、S3 などのオブジェクトストレージなんかを利用するのも手だと思うけど、そのためのツールを準備したらその分容量を食うわけで、COPY の方がマシなケースもそれなりにありそう。

ここまでの実際の全体のコードは以下。

heroku-app-info/bin/build at main · wtnabe/heroku-app-info

今回やりたかったこと

Heroku CLI に丸ごと依存してる gem なので Ruby 環境と Node.js 環境両方必要ということで、

する方法とした。できたものはそんなたいしたものじゃないし、直接 API 叩けばいいじゃないと思うかもしれないんだけど、PostgreSQL のバージョン情報とか謎の API があって、いったんそこは真面目に作り込むのをやめた。

でもまぁ、こんな感じでも少なくとも自分に都合のいい rubygems を実行するための Docker コンテナを作ることができるのは分かった。あとはこれを定期的に更新しておけば基本的にはオッケー。

More