HerokuのCloud Native Buildpacksを使ってCloud RunでSinatraアプリを動かしてみた
動機
やっぱりRubyからカジュアルにJavaScript runtimeを呼びたい
この10日間くらい、Ruby から JavaScript のライブラリの解釈の結果を取得できれば助かるなぁと考えていた。
ProseMirror/prosemirror-model: ProseMirror’s document model
Promise Mirror というブラウザ上の WYSIWYG エディタを作れるキットが自身のデータ構造をもとに HTML に変換する処理を、JavaScript 以外から利用できればいろいろできること増えるよなぁという話。
脱線を含めて経緯としてはこんな感じ。
- Node.js のフル仕様は要らないから Duktape で処理できないか?と思ったけど Vite から esbuild で ES5 に変換する処理が stack level を超えて断念
- フルマネージド環境で異なる言語 runtime をミックスして使うってイマドキはあんまり流行らないんじゃないかなぁ…。docker compose 的な感じで別プロセスの Node.js か何かに投げて処理するのがいいのか?
- Dockerfile も compose.yaml もメンテしたくないんだ!
- そう言えば Heroku のもとになった Cloud Foundry みたいなものを使えばやれるのでは?
- 今はもう Cloud Native Buildpacks ですよ
ということで Cloud Native Buildpacks でやりたいことができるか試してみたよ。
※ 実は去年 Docker の multi-stage build を調べていたときにこの Cloud Native Buildpacks が multi-stage build を利用していることで理解が深まったんだけど、当の Buildpack の利用に思い至らなかった。どうして…。
Ruby x Node.js x yarnを利用した静的サイト生成用のDocker imageをできるだけ素朴に構築する (2022-05-03) | あーありがち
結論
Cloud Native Buildpacks · Cloud Native Buildpacks
- Cloud Native Buildpacks を使えば Heroku のように(一定の規約を守った)アプリケーションのコードさえあれば Docker image を作ることができる
- Docker image からコンテナを実行できる環境さえあればどこでも同じように動作する(注意点アリ)
- さらに継続的にメンテされている Buildpack を使っていれば、自分で Dockerfile をメンテしなくても自動的にセキュリティなどの対策を期待できる
- つまり、Heroku のような環境のうち、実行コンテナイメージのビルドまでの部分については、どこの実行環境を利用していてもその恩恵に与れる
これで Dockerfile とおさらばだ!
成果物
まずはじめに成果物のリポジトリ。この中身を覗いて、動かしてみるとやりたいことがわかると思う。
Cloud Native Buildpacksとは
リンク先に詳細な解説があるので参照してもらうといい。
Cloud Native Buildpacksの主要なBuilderを調べてみた - ISID テックブログ
あとは蛇足。
かつて一世を風靡したHeroku buildpackとは
Cloud Native Buildpacks の情報に当たると特に buildpack という言葉が何回も出てきてややこしいので、そこだけ。
もともと自分は Heroku の buildpack に興味があって、その時はこうなっていた。
- buildpack とは特定のツールを導入するための手順と実際のインストール用のコードが収まったもの
- 例えば Ruby のインストールや Nginx を前段に置くといったセットアップはこの buildpack を用意することで済ませていた
実際の buildpack の挙動は以下のような手順を踏む。
- detect
- リポジトリ内のコード上のルールをもとに必要な言語 runtime を特定
- compile
- 所定のルールに従ってアプリケーションを実行可能な状態にビルド
detect 時に成功の status を返せば compile で実際に言語をインストールする、といった具合になっていて、Nginx みたいなアプリケーションの実行言語に関係なくインストールしたいものは detect を常時成功扱いにする、といった感じで調整する。
今の Cloud Native Buildpacks でも基本は同じで、
- detect
- build
と呼び方が変わったようだ。
Builderとは
上に書いた buildpack にはアプリケーションの実行を担う OS 部分の話が含まれていない。Heroku の場合、Stack と呼ばれるもので OS やそこにインストールされるツールが決まっており、例えば Heroku-22 stack は Ubuntu 22 LTS をベースに Heroku が調整したものになっている。
Cloud Native Buildpacks における Builder は
- buildpack を束ねる
- ベースイメージを規定する
- その他
以上の役割を担う。Ruby buildpack を含むものとしては具体的には以下のような Builder がある。
- heroku/cnb-builder-images: Recipes for building Heroku’s Cloud Native Buildpacks builder images
- Heroku は v2 と v3 それぞれに対応する builder がある。v2 と v3 で一部 build 時の挙動、できあがったコンテナの挙動が違うので注意
- GoogleCloudPlatform/buildpacks: Builders and buildpacks designed to run on Google Cloud’s container platforms
- v1 は Ruby buildpack が含まれていないようだが、2023-12-16 時点で GitHub 上から辿れる Builder には Ruby buildpack も含まれている
今回やったこと
- heroku/cnb-builder-images: Recipes for building Heroku’s Cloud Native Buildpacks builder images を利用
- Heroku の v3 対応版(v2 は明確に deprecated 扱いになっているため)
- アプリケーションを構築し、pack コマンドで Docker image をビルド
- docker run で動作を確認
- Heroku に deploy、動作を確認
- Cloud Run に deploy、動作を確認
手慣れた Ruby アプリで試したかったので、Ruby の buildpack があると分かっていた Heroku の builder を利用した。その時ハマったことは最後の注意点に挙げるが、期待通り 「アプリケーションのコードの様子から自動的に」Docker image がビルドされ、できあがった Docker image は普通に Heroku でも Cloud Run でも動作した。
ということで少なくともよくある Web アプリについては Docker image を作るために Dockerfile を書く必要はなくなった。これはめでたい。
今後の課題
web
プロセスしか動かしていないのでworker
がどうなるか確認したい- Heroku の場合はそのまま Dyno になってくれそうだが Cloud Run は Service ではなく Job にする必要があるはず
build時の注意点
Heroku builder (v3)のRubyのバージョン指定はGemfile.lockで
heroku/buildpacks-ruby: Heroku’s Cloud Native Buildpack for Ruby applications.
v2 までは .ruby-version
を参照して動いてくれていたが、v3 では Gemfile.lock
でバージョンを指定していない場合は default のバージョンで動作する。これも 2023-12-16 時点の話で、もしかしたらその後変わっているかもしれない。
assets:precompile taskを作ればいい具合に処理してくれる
Sinatra には便利な rake task 群はデフォルトでは存在しないが、Heroku の Ruby buildpack は assets:precompile task があればいい具合に処理してくれるので、手動でこの task に実行させたいコマンドをセットするとよい。
実行時の注意点
Herokuは従来のProcfileのままだと動かない
Procfile には
web: bundle exec puma -c config/puma.rb -p ${PORT:-3000}
のように書くが、今回ハマったのは Heroku の container stack ではなぜか bundle exec puma -c ...
っていうコマンドがあるということになっていて、どうしてもサーバの起動ができなかった。
仕方ないので
./bin/start-web
という sh script を用意して上の内容を以下のように作り、
#! /bin/sh
bundle exec puma -c config/puma.rb -p ${PORT:-3000}
Procfile からはこれを呼ぶことにした。
web: ./bin/start-web
Heroku さん、しっかりしてください…。