うなすけとあれこれ

2021年06月05日

Electronで作っているPocketリーダーアプリの近況報告

screenshot

ElectronでPocketリーダーアプリを作っています | うなすけとあれこれ

PocketのリーダーアプリをElectronで開発しています。 commit logを見返すと昨年11月頃から開発を進めており、bosyuでベータテスターとして何名かの方々に試してもらってからは、2週間くらいに1度の頻度リリースを行っており、現在 v0.16.0 になっています。

開発中のアプリのベータテスターを募集します!「あとで読む」の管理などにPocketを使っている方をお待ちしています~~~~

Pocketリーダーアプリのベータテスター募集! | うなすけ @yu_suke1994
https://t.co/EHMUPSyXft #bosyu

— うなすけ (@yu_suke1994) January 9, 2021

アプリ自体は、Pocketに保存している、あとで読むための記事をサクサクと読んでいくことにフォーカスしています。キーボードのみで記事リストを上下、アーカイブし、マウスで記事そのもののスクロールができるようになっています。具体的にどのようなものかは、以下の動画で大体掴めると思います。

さてbosyuがサービスをクローズするということになったので、このタイミングで進捗報告を兼ね、Discordに移行することにしました。以下から参加することができます。

https://discord.gg/5jjPx26x25

bosyuがやりとりが2週間途絶えるとやりとりができなくなる仕様ということもあり、それが締め切りの役割となってこれまでリリースを重ねてこれた部分があると思っています。それがDiscordに移行してその制限がなくなったときに、同じようなペースでリリースをできるかどうかはちょっとわかりません。

そしてゆくゆくは何らかの形で有料化するつもりでいます。とはいえ、試しに触ってもらってお金を払うに値するかどうか決められるようになっていて欲しく、そして一体何を課金要素とするのかについては何も考えられていません。

electron-jp

少し話がずれるのですが、electon-jpというSlackがあり、一時期は盛り上がっていたのですが、今ではただのRSS feedが流れるだけの場所になってしまっており、悲しみを感じています。ぽつぽつでもいいから、また人々の交流する場になってほしいと思っています。

Tweet
2021年06月05日
2021年05月17日

HTTP/3が喋れるcurlを定期的にbuildする

TL;DR

inductorさんが上のような記事を公開されています。ここで公開されているDockerfileによってbuildされたimageがあると嬉しいので、GitHub Actionsで定期的にbuildしたものをGitHub Container Registry上にホストすることにしました。

https://github.com/users/unasuke/packages/container/package/curl-http3

結論としてはこれだけなのですが、docker imageのtagについてちょっと手間取ったのでそれについて書こうと思います。

Docker imageのtagをある程度自由に書きたい

このimageをbuidするにあたり、 curl-http3 というimageに対して以下のtagを付けたいと思っていました。

GitHub ActionsでDocker buildを行いたいとなった場合には、以下のDocker公式が提供しているactionを使用するのが定石でしょう。

https://github.com/marketplace/actions/build-and-push-docker-images

そして、 docker/build-push-action@v2 を利用して動的なtagを付けようと、このようなYAMLを書きました。

- run: date +%Y-%m-%d
  id: date # 結果を参照できるようにidをつけておく
- run: foobar
- name: Build and push
  uses: docker/build-push-action@v2
  with:
    context: quiche
    push: true
    tags:
      - ghcr.io/unasuke/curl-http3:quiche-${{ steps.date.outputs.result }} # ここで dateの結果を利用したい
      - ghcr.io/unasuke/curl-http3:quiche-latest
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new

${{ steps.date.outputs }} というのは、 idがdateとなっているstepの出力を展開させるための記法です。

https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idoutputs

しかしここで文字列展開はできないらしく、以下のエラーによってActionを実行することができませんでした。

The workflow is not valid. .github/workflows/build.yml (Line: 32, Col: 13): A sequence was not expected .github/workflows/build.yml (Line: 70, Col: 13): A sequence was not expected

(どちらにせよ、上に書いたようなYAMLの内容では dateコマンドの結果を再利用することはできません、 ::set-output などの記法を適切に使用する必要があります)

どうすればいいでしょうか?

docker/metadata-action を使う

docker/metadata-action を使うと、ここで行いたいことができるようになります1

https://github.com/marketplace/actions/docker-metadata-action

もう一度、どのようなタグをつけたいかをおさらいします。

ここで、quiche版のみに注目して考えてみます。まず、quiche-latest については固定値なので docker/build-push-action@v2 だけでも実現できますが、今問題になっているのは、上でも述べたように quiche-2021-05-01 などのビルドした時点での日付が入っているものです。

これは、docker/metadata-action では type-schedule を指定することで実現できます。

https://github.com/marketplace/actions/docker-metadata-action#typeschedule

- uses: docker/metadata-action@v3
  id: meta
  with:
    images: ghcr.io/unasuke/curl-http3
    tags: |
      type=schedule,pattern={{date 'YYYY-MM-DD'}},prefix=quiche-
      type=raw,value=quiche-latest

- name: Build and push
  uses: docker/build-push-action@v2
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}

具体的には、上のような指定を書くことで quiche-2021-05-01 のようなtagを付けたimageをbuildすることができます。注意すべきは、この type=schedule で指定したtagが付与されるのはScheduled eventsの場合だけなので、pushしたタイミングで実行されるactionではtagが付きません。

他にもcommit hashを指定することもできます。

最終的に、 quiche-latestquiche-<YYYY-MM-DD>quiche-<commithash> の3種類のtagを付けるために、以下のようなYAMLを記述しました。

- uses: docker/metadata-action@v3
  id: meta
  with:
    images: ghcr.io/unasuke/curl-http3
    tags: |
      type=schedule,pattern={{date 'YYYY-MM-DD'}},prefix=quiche-
      type=raw,value=quiche-latest
      type=sha,prefix=quiche-

- name: Build and push
  uses: docker/build-push-action@v2
  with:
    context: quiche
    push: true
    tags: ${{ steps.meta.outputs.tags }}

実際に動いているWorkflowの定義は https://github.com/unasuke/curl-http3/blob/master/.github/workflows/build.yml にあります。

まとめ

HTTP/3が喋れるcurlを定期的にbuildして unasuke/curl-http3 に置いてあります。

また、GitHub Actionsにおいてdocker imageのtagをある程度柔軟に指定したい場合、 docker/metadata-action が解決策になるかもしれません。


  1. もともとは crazy-max/ghaction-docker-meta@v2 でしたが、この記事を書いている間にdocker org公式でメンテナンスされるようになっていました。すごい! https://github.com/docker/metadata-action/pull/78 

Tweet
2021年05月17日
2021年05月10日

VoiceMeeter Bananaだけでポン出しをする

ミーティングやLT会でポン出しがしたい

昨今の社会情勢から、オンライン上で会議をしたり、LT会に参加したりということが増えてきました。そうすると、たまに拍手音などのちょっとした効果音を流したいという場面も出てきます。そのような「ポン出し」を手軽にできる方法が無いものでしょうか。

VoiceMeeter Banana

そこで、VoiceMeeter Bananaというソフトウェアを使います。

VB-Audio VoiceMeeter Banana (公式サイト)

VoiceMeeter Bananaは仮想ミキサーソフトウェアで、配信用途でその使用方法を紹介している記事が沢山あります。そのようなものを見たことがある方もいらっしゃるでしょう。以下にVoiceMeeter Bananaの紹介や使い方について記載されている記事をいくつか記載しました。

他のアプリと組み合せたポン出し

リモート飲み会やテレビ会議でBGMや効果音を流して和もう! (ソフトウェアミキサーで遊ぶ)

上記の記事にもあるように、VLCなどのメディアプレイヤーとVoiceMeeter Bananaを組み合わせればポン出しは可能です。

しかしVLCを使った場合だと、複数の音を出し分けたい場合に、その種類の数だけウィンドウを開く必要があり少々不便でした。

Macro Buttonsで音を再生する

ところでVoiceMeeter BananaにはMacro Buttonsという機能があります。VoiceMeeter Bananaをインストールしたときに同時にインストールされるものです。

Macro Buttons

このMacro Buttonsでは様々な操作を行うことができます。どのようなことが可能なのかは公式のマニュアルに記載があります。

https://vb-audio.com/Voicemeeter/VoicemeeterBanana_UserManual.pdf

このなかの Recorder.load を使用することで音声を流すことができます。この動画の1:30あたりからMacro Buttonsの使い方についての説明が始まります。

せっかくなのでどのように使用するか、ここでも書いていこうと思います。

まずMacro Buttonsを起動し、どれかのボタンを右クリックすることで設定画面が開きます。

Macro Buttonsの設定画面を開く

ここで、"Request for Button ON / Trigger IN:“ に Recorder.load = "ここに音声ファイルの場所" と書きます。

Macro Buttonsの設定例

音声ファイルの場所はエクスプローラーからコピーするのが手軽でしょう。

explorerからpathのコピー

そして、このように設定したMacro Buttonをクリックすると、VoiceMeeter Banana上のカセットテープの部分にファイルが読み込まれて再生される様子が確認できます。

ポン出しの様子

注意点として、一度に再生できる音声はひとつだけのようなので、何度もクリックしてエコーがかかったような状態にすることはできません。

最後に

この記事ではMacro Buttonsで音声を流す方法について触れましたが、押している間だけマイクをミュートするといったこともできるようです。VoiceMeeter Bananaの説明書には、もっと色々な機能が記載されています。英語ではありますが、解説記事だけではなく一度説明書を読んでみることをオススメします。(僕はそれでMacro Buttonsの使い方を知りました)

また、VoiceMeeter BananaはDonationwareということもあり無料で使うことができますが、活用しているという方は是非寄付をしてみてはいかがでしょうか。

Tweet
2021年05月10日
2021年05月10日

電子辞書 'Brain' 上でRuby 3.0をビルドするのには○○時間かかる

Brain上ではLinuxが動きます、つまり

puhitakuさんが、SHARPの電子辞書 「Brain」 上で動作するLinux distribusion 「Brainux」を開発、公開しています。

Linuxが動くと、色々なことができるようになります。

そこで、普段Rubyを使っている僕はあることを思いつきました。

「電子辞書上のLinuxでRubyをビルドすると何時間かかるんだろう?」

これってトリビアになりませんか?

やってみる

はじめに参考情報として、GCP e2-medium (vCPU 2, Mem 4GB) でbuildした場合は11分かかりました。

me@ruby-build:~$ time rbenv install 3.0.1
Downloading ruby-3.0.1.tar.gz...
-> https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.1.tar.gz
Installing ruby-3.0.1...
Installed ruby-3.0.1 to /home/me/.rbenv/versions/3.0.1


real    11m5.652s
user    16m57.434s
sys     1m57.655s

物理的な準備

という訳で、まずは電子辞書を入手することにしました。

メルカリで中古のBrain PW-SH2-B を購入し、OTGアダプタやmicroSDも合わせて購入して、以下のような環境を構築しました。

物理準備の様子

OS imageの準備

Brainuxのイメージは、 https://github.com/brain-hackers/buildbrain/releases でビルド済みのものが配布されています。ただ試すだけであればこれをmicroSDに焼くだけで済みます。しかし、これからRubyのbuildや、そのために必要なパッケージのインストール、他にも様々なことを試して遊ぼうとすると、配布されているOS含め3GBのイメージというのは少々心許ないです。 というのも、BrainuxはRaspberry Piのような起動時の容量自動拡張機能がまだ用意されていないため、microSDの空き容量一杯までFSを伸張されることがなく、焼いたimageの容量以上に使用することができません。

そこで「自分でビルドする」という選択が出てきます。これは一見ハードルが高そうですが、 https://github.com/brain-hackers/buildbrain をcloneしてREADMEに従いmakeを何度か実行するだけでimageは出来上がるので簡単です。(時間はそれなりにかかります)

また、この過程において、 image/build_imaege.sh を次のように変更することで、容量を自由に設定することができます。今回は6GB程度にしてみました。

diff --git a/image/build_image.sh b/image/build_image.sh
index 73aaf85..9b07fa5 100755
--- a/image/build_image.sh
+++ b/image/build_image.sh
@@ -25,7 +25,7 @@ for i in $(seq 1 7); do
     esac
 done

-dd if=/dev/zero of=${IMG} bs=1M count=3072
+dd if=/dev/zero of=${IMG} bs=1M count=6000 # ここをいじるだけ!

 START1=2048
 SECTORS1=$((1024 * 1024 * 64 / 512))

Swap領域の用意

build中に何度かメモリ不足によって失敗したので、多めにSwap領域を用意します。これも公式の導入に載っているものをそのまま実行すれば完了です。

https://github.com/brain-hackers/README/wiki/Tips>Swap

$ sudo dd if=/dev/zero of=/var/swap bs=1M count=2048
$ sudo mkswap /var/swap
$ sudo swapon /var/swap

ここでは2GB用意しましたが、buildの過程を見ていた限りでは公式の通り256MB程度でよさそうに見えました。

### ca-certificatesの用意 現時点での最新版リリースでは、起動したばかりの状態では証明書に問題があるためにHTTPS通信ができず、Rubyのソースコードをダウンロードすることができません。なので、公式のScrapboxにTODOとして記載されているコマンドを実行します。

https://scrapbox.io/brain-hackers/Brainux_TODO

$ sudo wget -O /etc/ssl/certs/cacert.pem http://curl.se/ca/cacert.pem
$ sudo update-ca-certificates -f

rbenv、ruby-buildの準備

Rubyをbuildするために、rbenvとruby-buildを導入します。Rubyそのもののソースコードをcloneしないのは、このほうがRubyのversion切り替えなどで楽なためです。

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# 公式記載の手順を諸々やっていく

ビルド、開始

それでは、timeコマンド経由でbuildして、どのくらい時間がかかるか見てみます。

$ time rbenv install 3.0.1

ビルド時間は12時間を突破し……

途中経過

終わりました。

終わり

なんと839分もかかりました。

終わり ズームイン

839分、わかりやすく書けば 13時間59分 となります。

$ time rbenv install 3.0.1
Downloading ruby-3.0.1.tar.gz...
-> https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.1.tar.gz
Installing ruby-3.0.1...
Installed ruby-3.0.1 to /home/me/.rbenv/versions/3.0.1


real    839m45.175s
user    689m25.604s
sys     40m30.894s

ということで、 「電子辞書上のLinuxでRubyをビルドするのにかかる時間は"13時間59分"」 です。

参考情報

このBrain上で cat /proc/cpuinfo した結果は以下になります。

$ cat /proc/cpuinfo
proceccor       : 0
model name      : ARM926EJ-S rev 5 (v51)
BogoMIPS        : 119.70
Features        : swp half thumb fastmult edsp java
CPU implementer : 0x41
CPU architecture: 5TEJ
CPU variant     : 0x0
CPU part        : 0x926
CPU revision    : 5

HArdware        : Freescale MXS (Device Tree)
Revison         : 0000
Serial          : 0000000000000000

また /sys/kernel/debug/clk/clk_summary より、CPU clockは240MHzでした。

ビルド時間はわかったけど、どのくらいの速度で動くの?

ここまでの内容を Omotesando.rb #62 で発表したところ、「Ruby自体はどのくらいの性能が出るものなんですか?」という質問がありました。そこで、性能評価をすることにしました。

どうやってベンチマークを取るか

Ruby自体のbuildのように、一般的なマシンである速度で動くRubyのプログラムが、Brain上だとどのくらいの速度で動くのか?というのを比較できるような指標が欲しいところです。

そこで、「Ruby 3x3」宣言の指標として使われた、Optcarrotを動かしてみることにしました。

Optcarrotは、Ruby core teamのmameさんが開発したNES Emulatorです。動作させたときのfps値を性能の指標として使用することができます。

やってみる

まずは冒頭での記載と同様、GCP e2-medium (vCPU 2, Mem 4GB) 上で実行してみました(同一インスタンス)

$ ruby -v -Ilib -r./tools/shim bin/optcarrot --benchmark examples/Lan_Master.nes
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-linux]
fps: 28.59686607413079
checksum: 59662

$ ruby -v -Ilib -r./tools/shim bin/optcarrot --benchmark --opt examp
les/Lan_Master.nes
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-linux]
fps: 87.00124542792264
checksum: 59662

結果、最適化前で28.5fps、最適化後で87.0fpsまで出ています。

これをBrain上で動かしてみた結果がこちらになります。

Optcarrot 結果

$ ruby -v -Ilib -r./tools/shim bin/optcarrot --benchmark examples/Lan_Master.nes
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [armv5tejl-linux-eabi]
fps: 0.11981534177902647
checksum: 59662

$ ruby -v -Ilib -r./tools/shim bin/optcarrot --benchmark --opt examples/Lan_Master.nes
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [armv5tejl-linux-eabi]
fps: 0.5181845210009312
checksum: 59662

最適化前で 0.11 fps、 最適化後で 0.51 fpsとなりました。

ということで、「電子辞書上のRubyでOptcarrotを動かすと出るfpsは最適化後で0.5fps」 です。

おわりに

こうしてこの世界にまた2つ新たなトリビアが生まれたわけですが、Rubyはaptからインストールできるのに何故わざわざビルドするのでしょうか。

理由として、現在のBrainuxがベースとしているのがDebian busterであり、apt経由でインストールされるRubyは2.5と古いものになっています。

Debian – buster の ruby パッケージに関する詳細

Ruby 2.5はもう Ruby core teamのメンテナンス対象から外れたEOLなバージョンということもあり、できるだけ最新のものを使うことが推奨されます。

このリリースをもって、Ruby 2.5 系列は EOL となります。 即ち、Ruby 2.5.9 が Ruby 2.5 系列の最後のリリースとなります。 これ以降、仮に新たな脆弱性が発見されても、Ruby 2.5.10 などはリリースされません。 ユーザーの皆様におかれましては、速やかに、より新しい 3.0、2.7、2.6 といったバージョンへの移行を推奨します。 https://www.ruby-lang.org/ja/news/2021/04/05/ruby-2-5-9-released/

となると、自分でビルドするしかないですね!

また、この記事でちょこちょこ言及しているように、Brainux自体にはまだまだ改善の余地が転がっている状態です。Brainは中古で数千円程度で入手できるので、みなさんもBrainuxで遊んでみてはいかがでしょうか。

Tweet
2021年05月10日
2021年04月01日

S3は巨大なKVSなのでRailsのCache storeとしても使える

s3_cache_store

S3 is a Key-Value store

Amazon S3 は、一意のキー値を使用して、必要な数のオブジェクトを保存できるオブジェクトストアです。

Amazon S3 オブジェクトの概要 - Amazon Simple Storage Service


Amazon S3の基礎技術は、単純なKVS(Key-Value型データストア)でしかありません。

Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする | DevelopersIO

Amazon S3の実体はKey-Value storeという事実は、既にご存知の方々にとっては何を今更というようなことではありますが、それでも初めて聞くときには驚かされたものです。

さて、Key-Value storeと聞いて一般的に馴染みが深いのはRedisでしょう。そして、RailsにおけるRedisの役割としてCache storeがあります。

2.6 ActiveSupport::Cache::RedisCacheStore Redisキャッシュストアは、メモリ最大使用量に達した場合の自動エビクション(喪失: eviction)をサポートすることで、Memcachedキャッシュサーバーのように振る舞います。

Rails のキャッシュ機構 - Railsガイド

ここで、あるアイデアが降りてきます。

「S3がKey-Value storeであるならば、Cache storeとしてS3を使うこともできるのではないか?」

それでは、実際に ActiveSupport::Cache::S3CacheStore の実装をやってみましょう。

Cache Storeを新規に作成する

そもそも、Cache storeを新規に作成することはできるのか、できるならどのようにすればいいのでしょうか。

activesupport/lib/active_support/cache.rb には、以下のような記述があります。

https://github.com/rails/rails/blob/v6.1.3.1/activesupport/lib/active_support/cache.rb#L118-L122

# An abstract cache store class. There are multiple cache store
# implementations, each having its own additional features. See the classes
# under the ActiveSupport::Cache module, e.g.
# ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
# popular cache store for large production websites.

つまり抽象クラスであるところの ActiveSupport::Cache::Store を継承し、必要なmethodを実装することにより作成できそうです。

新規に実装する必要のあるmethodは何でしょうか。コメントを読み進めていくと、以下のような記述が見つかります。

https://github.com/rails/rails/blob/v6.1.3.1/activesupport/lib/active_support/cache.rb#L124-L125

# Some implementations may not support all methods beyond the basic cache
# methods of +fetch+, +write+, +read+, +exist?+, and +delete+.

とあるように、 fetchwritereadexist?delete の実装をすればいいのでしょうか。もっと読み進めると、以下のような記述と実装があります。

https://github.com/rails/rails/blob/v6.1.3.1/activesupport/lib/active_support/cache.rb#L575-L585

# Reads an entry from the cache implementation. Subclasses must implement
# this method.
def read_entry(key, **options)
  raise NotImplementedError.new
end

# Writes an entry to the cache implementation. Subclasses must implement
# this method.
def write_entry(key, entry, **options)
  raise NotImplementedError.new
end

先程列挙した fetchwrite も内部では read_entry などを呼び出すようになっており、実際にはこれらのmethodを定義すればよさそうということがわかります。他にも Subclasses must implement this method. とされているmethodを列挙すると、以下のものについて実装する必要があることがわかりました。

S3の制限

ということで、まずは愚直に実装してみました。 read_entry の実装のみ抜き出すと、以下のようになります。

def read_entry(key, options)
  raw = options&.fetch(:raw, false)
  resp = @client.get_object(
    {
      bucket: @bucket,
      key: key
    })
  deserialize_entry(resp.body.read, raw: raw)
rescue Aws::S3::Errors::NoSuchKey
  nil
end

これは一見うまくいくように見えます。そこで、既存のテストケースを新規に実装したCache classを対象に実行してみると、次のようなメッセージで落ちるようになりました。

落ちる

落ちているテストの実体は以下です。

https://github.com/rails/rails/blob/v6.1.3.1/activesupport/test/cache/behaviors/cache_store_behavior.rb#L475-L483

def test_really_long_keys
  key = "x" * 2048
  assert @cache.write(key, "bar")
  assert_equal "bar", @cache.read(key)
  assert_equal "bar", @cache.fetch(key)
  assert_nil @cache.read("#{key}x")
  assert_equal({ key => "bar" }, @cache.read_multi(key))
  assert @cache.delete(key)
end

Cache keyの名前として2048文字のものを登録しようとしています。

ここで改めてAmazon S3のドキュメントを読むと、以下のような制限があることがわかりました。

キー名は一続きの Unicode 文字で、UTF-8 にエンコードすると最大で 1,024 バイト長になります。 オブジェクトキー名の作成 - Amazon Simple Storage Service

ということで、基本的なテストケースを通過させることがS3の制限上できません。

……というような話を Omotesando.rb #60でしたところ、「SHA256などでHash化するとどうか」というアイデアを頂きました。

def read_entry(key, options)
  raw = options&.fetch(:raw, false)
  resp = @client.get_object(
    {
      bucket: @bucket,
      key: ::Digest::SHA2.hexdigest(key),
    })
  deserialize_entry(resp.body.read, raw: raw)
rescue Aws::S3::Errors::NoSuchKey
  nil
end

そこで、このようにCache keyとして一度SHA2を通すことにより、cache key長の制限は回避することができました。

一部通るようになった

どこまでがんばるか

ここまで実行してきたテストは、 activesupport/test/cache/behaviors/cache_store_behavior.rb がその実体となります。

Cache storeのテストは、各storeについてのテストが activesupport/test/cache/stores/ 以下にあり、それらの内部で activesupport/test/cache/behaviors/ 以下にある程度まとめられた振る舞いをincludeすることによってstoreに実装されている振る舞いをテストする、という構造になっています。 例を挙げると、 RedisCacheStoreTestMemCacheStoreTest では EncodedKeyCacheBehavior をincludeしていますが、 FileStoreTest ではそうではありません。

ここでは一旦、およそ基本的な振る舞いのテストとなっているであろう CacheStoreBehavior の完走を目指して実装していきます。

Key長の課題を解決した時点で、失敗しているテストは以下3つです。

このうち、 test_delete_multitest_crazy_key_characters については実装を少し修正することによってテストが通るようになりました。しかし、 test_expires_in はそうもいきません。

test_expires_in をどうするか

このテストの内容は以下です。

https://github.com/rails/rails/blob/v6.1.3.1/activesupport/test/cache/behaviors/cache_store_behavior.rb#L392-L407

def test_expires_in
  time = Time.local(2008, 4, 24)

  Time.stub(:now, time) do
    @cache.write("foo", "bar")
    assert_equal "bar", @cache.read("foo")
  end

  Time.stub(:now, time + 30) do
    assert_equal "bar", @cache.read("foo")
  end

  Time.stub(:now, time + 61) do
    assert_nil @cache.read("foo")
  end
end

ここでは、Cacheの内容が指定した時間にちゃんとexpireされるかどうかを確認しています。テスト時に、各Cache storeにoptionとして expires_in を60として渡しており、その時間が経過した後にCache keyがexpireされて nil が返ってくることを確認しています。

このテストが落ちてしまっているのは、 Aws::S3::Errors::RequestTimeTooSkewed という例外が発生しているためで、これはリモートのS3とリクエストを送信しているローカルマシンの時刻が大幅にずれているために発生するものです。

テスト内で Time.now をstubし、2008年4月24日にリクエストを送信するようなテストになっているので、これをそのままなんとかするのは非常に困難 1 です。

では、テスト側を変更するのはどうでしょう。Too skewedで怒られるのであれば、tooでなければいいと思いませんか?

ということで、以下のように変更しました。(ある程度の余裕を持たせて120秒戻しました)

   def test_expires_in
-    time = Time.local(2008, 4, 24)
+    time = Time.current - 120.seconds

     Time.stub(:now, time) do
       @cache.write("foo", "bar")

こうすると、 CacheStoreBehavior に定義されているテストは全てpassしました。

通るようになった

gemにする

ここまでは既存のテストの使い回しなどの都合上、rails/rails の内部に実装していましたが、どうせならgemにしてしまいましょう。

https://github.com/unasuke/s3_cache_store

してしまいました。rubygems.orgでhostするほどのものでもないなと感じたので、使用したい場合は直接GitHubのURLを指定するようにしてください。

Pros/Cons

下らないひらめきがきっかけで実装したこのS3CacheStoreですが、既存のCache storeに対して優位となる点があるかどうか考えてみます。

Pros: 高い可用性

S3の可用性は「99.99%を提供する」とされており2、これは月間で4分程度、年間で1時間に満たない程度のダウンタイムが発生する程度です。これはRedisCacheStoreのバックエンドに同じAWSのElastiCacheを採用した場合のSLAと比較3すると、ElastiCacheは99.9%なので1ケタ高い可用性を持ちます。

S3は、S3CacheStoreを使用するRails appよりはるかに高い可用性を持つことは明らかでしょう。

Pros: (事実上)無限のCache storage

無限ではないですが……少なくともRedisやMemcachedをCache storeにした場合、S3はそれらよりはるかに大容量のCache storageとして振る舞うことができます。 例えばRedisの基本的なdata typeであるStringsは、サイズの上限が512MBとなっています4が、S3では1つのオブジェクトの最大サイズは5TBです5

そんなでかいサイズのものをCacheとして保存する意味はわかりませんが、とにかく大きなデータについてもCacheできます。

Cons: 遅い

……と言い切ってしまいましたが、本当に遅いのでしょうか?試してみましょう。

https://github.com/unasuke/s3_cache_store/blob/master/benchmark.rb

def bench_redis_cache_store
  store = ActiveSupport::Cache::RedisCacheStore.new({
    url: 'redis://localhost:6379'
  })
  redis_start = Time.now
  (1..COUNT).each do |e|
    store.write(e, e)
    store.read(e)
  end
  redis_duration = Time.now - redis_start
  puts "RedisCacheStore duration: #{redis_duration} sec (#{redis_duration / COUNT} s/key)"
end

def bench_s3_cache_store
  store = ActiveSupport::Cache::S3CacheStore.new({
    access_key_id: 'minioadmin',
    secret_access_key: 'minioadmin',
    region: 'us-east-1',
    endpoint: 'http://127.0.0.1:9000',
    force_path_style: true,
    bucket: BUCKET
  })
  s3_start = Time.now
  (1..COUNT).each do |e|
    store.write(e, e)
    store.read(e)
  end
  s3_duration = Time.now - s3_start
  puts "S3CacheStore duration: #{s3_duration} sec (#{s3_duration / COUNT} s/key)"
end

重要な部分のみ切り出したものを上に貼りました。

検証には、どちらもlocalのDockerで起動したRedisとMinIOを使用しました。では実行してみましょう。

$ ruby benchmark.rb
Fetching gem metadata from https://rubygems.org/.............
Fetching gem metadata from https://rubygems.org/.
# ...snip...

===== start benchmark ==========
RedisCacheStore duration: 1.2188961 sec (0.0012188961 s/key rw)
S3CacheStore duration: 9.5209312 sec (0.009520931199999999 s/key rw)
===== end benchmark ============

# ...snip...

何度か実行しても8~9倍の開きがあることがわかりました。よって、遅いですね。MinIOを使用しているのでS3とElastiCacheではまた違った結果になることが予想できますが、localhostに閉じた通信ですらこのような速度差が出ることを考えると、Cache storeとしては実用的ではないでしょう。

まとめ

Amazon S3の内部実装は Key-Value Storeです。


  1. ですよね? 

  2. https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/DataDurability.html 

  3. 実際にはS3のSLAはElastiCacheと同様に99.9%なので、SLAの範囲で比較すると差はないことになります。 https://aws.amazon.com/jp/s3/sla/https://aws.amazon.com/jp/elasticache/sla/ より 

  4. https://redis.io/topics/data-types#strings 

  5. https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/UsingObjects.html 

Tweet
2021年04月01日
2021年03月29日

distroless imageを実用する

build pyton dinstroless image

distroless image

distrolessは、Googleが提供している、本当に必要な依存のみが含まれているcontainer imageです。そこにはaptはおろかshellも含まれておらず、非常にサイズの小さいimageとなっています。余計なプログラムが含まれていないことは attack surfaceの縮小にも繋がり、コンテナのセキュリティについての事業を展開しているSysdig社が公開しているDockerfileのベストプラクティスとしてもdistroless imageを使うことが推奨されています。

Dockerfileのベストプラクティス Top 20 | Sysdig

軽量Dockerイメージに安易にAlpineを使うのはやめたほうがいいという話 - inductor’s blog

また先日、inductorさんがこのようなブログ記事を書き話題になりました。この記事からdistroless imageのことを知った方も多いと思います。その中で僕が趣味で作った distroless-ruby を取り上げてくださり、ありがたいことに僕の所有しているものの中で一番Star数が多いrepositoryになりました。

とはいえ申し訳ないことに、僕はRubyなどのスクリプト言語を使用する機会が多く、あまりdistrolessを活用してきませんでした。そこで、Rubyのdistroless imageを作成する過程で得た知見を元に、Pythonなど1バイナリで完結しないプログラムをdistrolessで動かす方法について調べてまとめました。

distrolessと共有ライブラリ

個人的に、distrolessはGoで書かれたプログラムとはとても相性が良いと考えています1

dostroless imageを使用するとき、例えばGoでバイナリを配置するだけの場合や、Pythonで共有ライブラリを静的にリンクした成果物をコンテナ内に配置できる場合は工夫しなくても build stage と copy stage を組み合せたmulti stage buildで済みます。

しかし、aptによってインストールした共有ライブラリを動的リンクする必要があったり、単純に外部のプログラムが必要な場合は提供されているdistroless imageをそのまま使うことができません。 「できない」と書いたものの、技術的には不可能ではありません。例えば $ sudo apt install foobar を実行した結果、追加されたファイルを列挙してdistroless image内に配置すればパッケージを使用することはできます。 でも、例えば 外部ライブラリが必要だとして、追加されるファイルが膨大な数になる場合はどうでしょうか。また。それらは特定のディレクトリにまとまっている訳でもないでしょう。

この記事では例として、orisano さん提供の「素のdistroless imageでは動かないPython script」を動かすことを考えてみます。

以下のような app.pyDockerfile は、そのままでは動きません。

import gmpy2
print("gmpy2")
FROM debian:buster-slim AS build
RUN apt-get update && apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc libpython3-dev libmpfr-dev libmpc-dev
RUN python3 -m venv /venv && /venv/bin/pip install --upgrade pip
FROM build AS build-venv
RUN /venv/bin/pip install --disable-pip-version-check gmpy2
FROM gcr.io/distroless/python3-debian10
COPY --from=build-venv /venv /venv
COPY . /app
WORKDIR /app
ENTRYPOINT ["/venv/bin/python3", "app.py"]
$ docker run --rm distroless-python-test
Traceback (most recent call last):
  File "app.py", line 1, in <module>
    import gmpy2
ImportError: libgmp.so.10: cannot open shared object file: No such file or directory

ここで、最低限必要なライブラリを含んだdocker imageを作成するために、distrolessと同様にbazelを使用します。

以下のような WORKSPACE 及び BUILD を準備します。

workspace(name='python-test')
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

git_repository(
    name = "rules_pkg",
    remote = "https://github.com/bazelbuild/rules_pkg.git",
    tag = "0.4.0"
)

load("@rules_pkg//pkg:deps.bzl", "rules_pkg_dependencies")
rules_pkg_dependencies()

load("@rules_pkg//deb_packages:deb_packages.bzl", "deb_packages")

http_file(
    name = "buster_archive_key",
    sha256 = "9c854992fc6c423efe8622c3c326a66e73268995ecbe8f685129063206a18043",
    urls = ["https://ftp-master.debian.org/keys/archive-key-10.asc"],
)

deb_packages(
  name = "depends_python_gmpy2_debian_buster",
  arch = "amd64",
  distro = "buster",
  distro_type = "debian",
  mirrors = ["https://ftp.debian.org/debian"],
  packages = {
    "libgmp10": "pool/main/g/gmp/libgmp10_6.1.2+dfsg-4_amd64.deb",
    "libmpc3": "pool/main/m/mpclib3/libmpc3_1.1.0-1_amd64.deb",
    "libmpfr6": "pool/main/m/mpfr4/libmpfr6_4.0.2-1_amd64.deb",
  },
  packages_sha256 = {
    "libgmp10": "d9c9661c7d4d686a82c29d183124adacbefff797f1ef5723d509dbaa2e92a87c",
    "libmpc3": "a73b05c10399636a7c7bff266205de05631dc4af502bfb441cbbc6af0a7deb2a",
    "libmpfr6": "d005438229811b09ea9783491c98b145c9bcf6489284ad7870c19d2d09a8f571",
  },
  pgp_key = "buster_archive_key",
)

http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "95d39fd84ff4474babaf190450ee034d958202043e366b9fc38f438c9e6c3334",
    strip_prefix = "rules_docker-0.16.0",
    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.16.0/rules_docker-v0.16.0.tar.gz"],
)

load(
    "@io_bazel_rules_docker//repositories:repositories.bzl",
    container_repositories = "repositories",
)
container_repositories()

load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
container_deps()

load(
    "@io_bazel_rules_docker//container:container.bzl",
    "container_pull",
)
container_pull(
  name = "python_distroless",
  registry = "gcr.io",
  repository = "distroless/python3-debian10",
  tag = "latest"
)
load("@depends_python_gmpy2_debian_buster//debs:deb_packages.bzl", "depends_python_gmpy2_debian_buster")
load("@io_bazel_rules_docker//container:container.bzl", "container_image")

container_image(
  name ="python_with_gmpy2_depends",
  base="@python_distroless//image",
  debs=[
    depends_python_gmpy2_debian_buster["libgmp10"],
    depends_python_gmpy2_debian_buster["libmpc3"],
    depends_python_gmpy2_debian_buster["libmpfr6"],
  ],
)

https://github.com/unasuke/distroless-with-additional-deps に同じものを用意しました。

このようなBazelによるbuild ruleを定義し、そのディレクトリで bazel run //:python_with_gmpy2_depends を実行すると、 bazel:python_with_gmpy2_depends というdocker imgaeが作成されます。これを FROM として指定した以下のDockerfileをbuild、runしてみると、gmpy2がちゃんと動くようになっているのが確認できます。

FROM debian:buster-slim AS build
RUN apt-get update && apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc libpython3-dev libmpfr-dev libmpc-dev
RUN python3 -m venv /venv && /venv/bin/pip install --upgrade pip
FROM build AS build-venv
RUN /venv/bin/pip install --disable-pip-version-check gmpy2
# FROM gcr.io/distroless/python3-debian10
FROM bazel:python_with_gmpy2_depends
COPY --from=build-venv /venv /venv
COPY . /app
WORKDIR /app
ENTRYPOINT ["/venv/bin/python3", "app.py"]

このようにして作成したimageは、元々のdistroless imageに加えて1.8MBしかサイズが増えておらず、必要最低限の依存のみを追加することができています。(dlayerで調べてみるとわかります)

Python及びRubyはdistrolessに向いているのか?

僕が思うに、あまり向いていません。

PythonやRubyのようなscript言語は、実行時に必要となるライブラリ群をまとめて1つないし複数個のバイナリとして固めることができません。がんばれば実行時に必要なファイルを列挙することもできるでしょうが、その作業と実際の配置を行うのは困難です。

そのような労力を惜しまず、上記のようなBazel ruleを記述してdistroless imageを使うのか、それともslim imageでやっていくのかは、distrolessにする作業のコストと、image sizeとattack surfaceを減らすことにより得られるメリットを比較して判断することになると思います。

そして多くの場合において、slim imageを使うことが最適解になるのではないかとも思っています。

……あと、そもそもexperimental扱いですし。

おわりに

この記事はZennにクロスポストしました。内容について面白かった、参考になったなどのお気持ちを「サポート」として頂けると非常に嬉しいです。

https://zenn.dev/unasuke/articles/5ee6e2067ab1ba

おまけ musl libcとglibcの違いって何

軽量なイメージのベースとして使われることも多いAlpine Linuxは、標準CライブラリにGNU C Library(glibc)ではなくmusl libcを採用しています。glibcとmusl libcは完全に同じ動作をするものではなく、現にRubyにおいては以下のような報告があります。

musl公式FAQには以下のような記述があります。

Is musl compatible with glibc? Yes and no. At both the source and binary level, musl aims for a degree of feature-compatibility, but not bug-compatibility, with glibc.

(抄訳) muslはglibc互換ですか? はいであり、いいえでもあります。ソースでもバイナリのレベルでも、muslはglibcと機能面においてある程度の互換性に焦点を当てており、バグの互換性についてはそうではありません。

https://www.musl-libc.org/faq.html

具体的な差異については以下のwikiにまとめられています。

https://wiki.musl-libc.org/functional-differences-from-glibc.html

見出しを抜き出して列挙すると

このあたり、用語に詳しくはないので頓珍漢なことを言っているかもしれません。原典にあたることを強く推奨します。

……というようにglibcとmusl libcには挙動の差異が存在します。これについて、musl libcに単純に置き換えてもいいかどうかはしっかり検証を行ったうえで実行する必要があるでしょう。

参考URL


  1. Javaや.Netについては知識が不足しているためなんとも言えません 

Tweet
2021年03月29日
2021年03月29日

Bazelを使ってRubyのdistroless imageを作る

docker layer of the distroless ruby image

きっかけ

元々、distroless-rubyは手作業で必要なファイルを抜き出して作成したものでした。

ただ、この方針ではsinatraなどRuby単体で完結するプログラムは手軽に動かすことはできても、外部に依存するものがあるプログラムを動かすことはできません。具体的な例を挙げるならPostgreSQLをRubyで使用するために必要なpq gemはlibpq-devをapt経由でインストールする必要がありますが、distroless image内にはaptが存在しないのでインストールすることができません。multi stage buildを使用し、aptによって追加されたファイルを持ってくることも出来無くはないですが、依存する共有ライブラリ全てに対してその作業を行うのはいささか手間がかかり過ぎます。

で、あるならば、本家distroless imageと同じくBazelによりdistroless-ruby imageを作るほうが色々と融通が効いて良いというものでしょう。ということで、作ってみます。

nodejsみたくわりとかんたんかと思ったけど、rubyはバイナリが入っているtar.gz提供してないみたいなので、ソースからコンパイルするか、インストーラーを使ってインストールするかになっちょうので、面倒。 https://t.co/dDIbWY5Oqf

— Ian Lewis (@IanMLewis) March 8, 2021

しかし、Google Cloudの中の方、Ianさんがこのように発言されていることから、一筋縄ではいかなそうであることが予想できますね。「コンテナ内部でRubyをbuildするのの何が面倒なのだ?」と僕も当初は考えましたが、 Googleが開発する最新ビルドツール「Bazel」を使ってみよう | さくらのナレッジ において、

イメージの作成時にコンテナ内で処理を実行することはできない

と記述されています。これが書かれたのは2016年ではあり現在は事情が変わっていることも予想されますが、やはり大変なのではないかという雰囲気を感じます。

やってみる

とはいえやってみましょう。

以下手順は2021年3月15日付近、環境はUbuntu 20.04 (focal) LTS (WSL2)、distroless本家のHEADが 84e71ef9eda0d の状態で行っています。

また、僕はdistrolessを趣味でやっており、Bazelについても初心者なので誤っている点があると思います。そのような点がありましたら、僕に連絡するかご自身で訂正する記事を公開してもらえたら嬉しいです。

Python2を準備する

Bazelとdistrolessのbuildにあたっては /usr/bin/env python が2系である必要があるので、手元の環境をそのようにします。新しめのubuntuでは $ sudo apt install python-is-python2 でそのようになります1

Bazelを準備する

distroless imageの構築にはBazelが必要なので、インストールします。

Installing Bazel on Ubuntu - Bazel 4.0.0

上記リンクにてubuntuの場合のインストール方法が記載されています。ただ、distrolessで使用しているBazelのversionは 3.4.1 なので、インストールに必要なコマンドの一連は以下のようになります。

$ sudo apt install curl gnupg
$ curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg
$ sudo mv bazel.gpg /etc/apt/trusted.gpg.d/
$ echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
$ sudo apt update && sudo apt install bazel-3.4.1
$ sudo ln -s /usr/bin/bazel-3.4.1 /usr/bin/bazel

distroless repositoryのclone

https://github.com/GoogleContainerTools/distroless/ を手元にcloneします。

ここからの手順はcloneしてきたdistroless directory下で実行しています。

updateWorkspaceSnapshots.sh を実行する

おもむろに $ ./updateWorkspaceSnapshots.sh します。

というのも、distroless imageのbuildの過程で最新のsecurity patchが適用されたdebian packageを取得するために、 snapshot.debian.org に対して checksums.bzl 内に記載してあるsnapshot versionを取得しにいくのですが、現在のHEADにおいてサーバーが不安定なのか取得に何度も失敗する状況2 3 なので、 updateWorkspaceSnapshots.sh を実行して最新のreleaseを取得しにいくように checksums.bzl を更新します。(最新の状態に更新してもしばしば失敗するので、そういうものだと思ったほうがいいのかもしれません)

また、これはupstreamにおいては毎月実行されているものなので、いずれ不要になる4でしょう。

bazel でcontainer imageを作成してみる

では実際にBazelで実行できるtaskを一覧するために、以下のコマンドを実行します。

$ bazel query ...
//package_manager:util_test
//package_manager:parse_metadata_test
//package_manager:dpkg_parser.par
//package_manager:dpkg_parser
//package_manager:version_utils
//package_manager:util
//package_manager:parse_metadata
//nodejs:nodejs14_debug_arm64_debian10
//nodejs:nodejs12_debug_arm64_debian9
//nodejs:nodejs12_debug_arm64_debian10
//nodejs:nodejs12_arm64_debian9
//nodejs:nodejs12_arm64_debian10
//nodejs:nodejs10_debug_arm64_debian9
............

637ものbuild対象がありました。言語とアーキテクチャとroot/nonrootなどの組み合わせがあるので膨大な量になります。

ひとまず、単純と思われるccについて確認してみるため、以下のコマンドを実行します。

$ bazel run //cc:debug_nonroot_amd64_debian10
INFO: Analyzed target //cc:debug_nonroot_amd64_debian10 (89 packages loaded, 6894 targets configured).
INFO: Found 1 target...
INFO: From ImageLayer base/static_nonroot_amd64_debian10-layer.tar:
Duplicate file in archive: ./etc/os-release, picking first occurrence
Target //cc:debug_nonroot_amd64_debian10 up-to-date:
  bazel-bin/cc/debug_nonroot_amd64_debian10-layer.tar
INFO: Elapsed time: 6.957s, Critical Path: 3.44s
INFO: 31 processes: 31 linux-sandbox.
INFO: Build completed successfully, 61 total actions
INFO: Build completed successfully, 61 total actions
Loaded image ID: sha256:c0003d5371b5168ece90447caee6fee576e3cc9ad89e3773386c5cd4448a60bb
Tagging c0003d5371b5168ece90447caee6fee576e3cc9ad89e3773386c5cd4448a60bb as bazel/cc:debug_nonroot_amd64_debian10

これにより、ローカルに bazel/cc:debug_nonroot_amd64_debian10 という docker imageができています5

$ docker image ls
REPOSITORY      TAG                                 IMAGE ID       CREATED         SIZE
bazel/cc        debug_nonroot_amd64_debian10        c0003d5371b5   51 years ago    22.2MB

このimageの中身をlayer可視化ツールのひとつ、 orisano/dlayer で見てみましょう。

$ docker save -o cc.tar bazel/cc:debug_nonroot_amd64_debian10
$ dlayer -f cc.tar

====================================================================================================
 1.8 MB          $ bazel build ...
====================================================================================================
 198 kB          etc/ssl/certs/ca-certificates.crt
  62 kB          usr/share/doc/tzdata/changelog.gz
  35 kB          usr/share/common-licenses/GPL-3

......snip (zoneinfoやca-certificates的な)

====================================================================================================
  15 MB          $ bazel build ...
====================================================================================================
 2.7 MB          usr/lib/x86_64-linux-gnu/libcrypto.so.1.1
 1.7 MB          lib/x86_64-linux-gnu/libc-2.24.so
 1.1 MB          lib/x86_64-linux-gnu/libm-2.24.so
 655 kB          usr/bin/openssl
 469 kB          usr/lib/x86_64-linux-gnu/gconv/libCNS.so
 443 kB          usr/lib/x86_64-linux-gnu/libssl.so.1.1
 236 kB          usr/lib/x86_64-linux-gnu/gconv/BIG5HKSCS.so

......snip (openssl的な)

====================================================================================================
 1.1 MB          $ bazel build ...
====================================================================================================
 1.1 MB          busybox/busybox
   0  B          busybox/[
   0  B          busybox/[[
   0  B          busybox/acpid

......snip (busybox的な)

====================================================================================================
 1.9 MB          $ bazel build ...
====================================================================================================
 1.6 MB          usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.22
 184 kB          usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0
  93 kB          lib/x86_64-linux-gnu/libgcc_s.so.1
  56 kB          usr/share/gcc-6/python/libstdcxx/v6/printers.py

......snip (glibc的な)

このように、いくつかのlayerに分かれて必要なファイルがガッチャンコされているんだな、ということがわかります。

buildされるときの処理を追う

さて、container imageのbuildが成功したところで、今buildしたcc imageのbuild ruleを見てみます。

https://github.com/GoogleContainerTools/distroless/blob/84e71ef9eda0dd498687aa306e4a812ac477a8f8/cc/BUILD

package(default_visibility = ["//visibility:public"])

load("//base:distro.bzl", "DISTRO_PACKAGES", "DISTRO_SUFFIXES")
load("@io_bazel_rules_docker//container:container.bzl", "container_image")
load("//:checksums.bzl", "ARCHITECTURES")

# An image for C/C++ programs
[
    container_image(
        name = ("cc" if (not mode) else mode[1:]) + "_" + user + "_" + arch + distro_suffix,
        architecture = arch,
        base = "//base" + (mode if mode else ":base") + "_" + user + "_" + arch + distro_suffix,
        debs = [
            DISTRO_PACKAGES[arch][distro_suffix]["libgcc1"],
            DISTRO_PACKAGES[arch][distro_suffix]["libgomp1"],
            DISTRO_PACKAGES[arch][distro_suffix]["libstdc++6"],
        ],
    )
    for mode in [
        "",
        ":debug",
    ]
    for arch in ARCHITECTURES
    for user in [
        "root",
        "nonroot",
    ]
    for distro_suffix in DISTRO_SUFFIXES
]

これを見ると、modeごとに、architectureごとに、userごとに、distroごとにcontainer imageを作成していそうなことが読みとれます。そのcontainer imageには libgcc1libgomp1libstdc++6 が含まれていることも予想できます。DISTRO_PACKAGESDISTRO_SUFFIXbase/distro.bzl から、 ARCHITECTURESchecksums.bzl から来ていることもわかります。

ccについてはそれほど行数もないこともあり、大体の処理を把握できました。それではdistrolessなRubyを作成するにあたり、近いことをしていると予想できるPython3の場合を見てみます。147行と少し長いので、ここぞ!と思われる部分を抜き出します。

https://github.com/GoogleContainerTools/distroless/blob/84e71ef9eda0dd498687aa306e4a812ac477a8f8/experimental/python3/BUILD#L36-L72

container_image(
    name = ("python3" if (not mode) else mode[1:]) + "_root_" + arch + distro_suffix,
    architecture = arch,
    # Based on //cc so that C extensions work properly.
    base = "//cc" + (mode if mode else ":cc") + "_root_" + arch + distro_suffix,
    debs = [
        DISTRO_PACKAGES[arch][distro_suffix]["dash"],
        DISTRO_PACKAGES[arch][distro_suffix]["libbz2-1.0"],
        DISTRO_PACKAGES[arch][distro_suffix]["libc-bin"],
        DISTRO_PACKAGES[arch][distro_suffix]["libdb5.3"],
        DISTRO_PACKAGES[arch][distro_suffix]["libexpat1"],
        DISTRO_PACKAGES[arch][distro_suffix]["liblzma5"],
        DISTRO_PACKAGES[arch][distro_suffix]["libmpdec2"],
        DISTRO_PACKAGES[arch][distro_suffix]["libreadline7"],
        DISTRO_PACKAGES[arch][distro_suffix]["libsqlite3-0"],
        DISTRO_PACKAGES[arch][distro_suffix]["libssl1.1"],
        DISTRO_PACKAGES[arch][distro_suffix]["zlib1g"],
    ] + [DISTRO_PACKAGES[arch][distro_suffix][deb] for deb in DISTRO_DEBS[distro_suffix]],
    entrypoint = [
        "/usr/bin/python" + DISTRO_VERSION[distro_suffix],
    ],
    # Use UTF-8 encoding for file system: match modern Linux
    env = {"LANG": "C.UTF-8"},
    symlinks = {
        "/usr/bin/python": "/usr/bin/python" + DISTRO_VERSION[distro_suffix],
        "/usr/bin/python3": "/usr/bin/python" + DISTRO_VERSION[distro_suffix],
    },
    tars = [
        "//experimental/python2.7:ld_so_" + arch + "_cache.tar",
    ],
)
for mode in [
    "",
    ":debug",
]
for arch in ARCHITECTURES
for distro_suffix in DISTRO_SUFFIXES

base imageを先程見たccとし、debsにPython3が必要するpackageを、かつdistroごとに必要とされているdeb packagesを追加、環境変数の設定、entrypointの設定などを行っています。

ここで指定できるattributeは、以下にまとめられています。

https://github.com/bazelbuild/rules_docker#container_image-1

Rubyの distroless imageをbuildするためのruleを書いてみる

それでは、Python3のbuild ruleを参考にして、Rubyのものを書いてみます。

debian10(buster)とdebian9(stretch)において、Rubyをインストールするための情報は以下に記載されています。

ここから、ruby2.5 (debian10の場合) をインストールするために必要な依存パッケージを全て列挙6し、そのうちまだ記載されていないものを WORKSPACEdpkg_list に追加し、 updateWorkspaceSnapshots.sh を実行して package_bundle_{architecture}_debian{9,10}.versions を更新します。おそらくこれで、追加したdeb packageをこのworkspace以下でcontainer imageにインストールすることができるようになります。

https://github.com/unasuke/distroless/commit/b7a069e3ba4d8a

diff --git a/WORKSPACE b/WORKSPACE
index 3a16ab7..1e1256f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -246,6 +261,23 @@ load(
             "python3-distutils",
             "python3.7-minimal",

+            #ruby
+            "libgdbm6",
+            "libgdbm-compat4",
+            "libncurses6",
+            "libruby2.5",
+            "libyaml-0-2",
+            "rake",
+            "ruby",
+            "rubygems-integration",
+            "ruby-did-you-mean",
+            "ruby-minitest",
+            "ruby-net-telnet",
+            "ruby-power-assert",
+            "ruby-test-unit",
+            "ruby-xmlrpc",
+            "ruby2.5",
+
             #dotnet
             "libcurl4",
             "libgssapi-krb5-2",

次に、debian9と10で異なるpackageをインストールしたい場合の差異を抜き出してまとめます。

https://github.com/unasuke/distroless/commit/a0de61991f750cc1a15

# distribution-specific deb dependencies
DISTRO_DEBS = {
    "_debian9": [
        "libgdbm3",
        "libncurses5",
        "libruby2.3",
        "libssl1.0.2",
        "libtinfo5",
        "ruby2.3",
    ],
    "_debian10": [
        "libgdbm6",
        "libgdbm-compat4",
        "libncurses6",
        "libruby2.5",
        "libssl1.1",
        "libtinfo6",
        "ruby-xmlrpc",
        "ruby2.5",
    ],
}

あとは共通して必要なdeb packageを container_imagedebs に列挙し、その他諸々を整えます。

https://github.com/unasuke/distroless/commit/a0de61991f750cc1a159

この時点で bazel query ... を実行すると、Rubyに関係するtaskが出現しています。

% bazel query ... | grep ruby
//experimental/ruby:ruby_nonroot_arm64_debian9
//experimental/ruby:ruby_root_arm64_debian9
//experimental/ruby:ruby_nonroot_arm64_debian10
//experimental/ruby:ruby_root_arm64_debian10
//experimental/ruby:ruby_nonroot_amd64_debian9
//experimental/ruby:ruby_root_amd64_debian9
//experimental/ruby:ruby_nonroot_amd64_debian10
//experimental/ruby:ruby_root_amd64_debian10
//experimental/ruby:debug_nonroot_arm64_debian9
//experimental/ruby:debug_root_arm64_debian9
//experimental/ruby:debug_nonroot_arm64_debian10
//experimental/ruby:debug_root_arm64_debian10
//experimental/ruby:debug_nonroot_amd64_debian9
//experimental/ruby:debug_root_amd64_debian9
//experimental/ruby:debug_nonroot_amd64_debian10
//experimental/ruby:debug_root_amd64_debian10

では、 $ bazel build //experimental/ruby:all でこれらを全部buildしてみます。

$ bazel build //experimental/ruby:all
INFO: Build option --host_force_python has changed, discarding analysis cache.
INFO: Analyzed 16 targets (61 packages loaded, 7043 targets configured).
INFO: Found 16 targets...
INFO: From ImageLayer base/static_root_arm64_debian10-layer.tar:
Duplicate file in archive: ./etc/os-release, picking first occurrence
INFO: From ImageLayer base/static_root_arm64_debian9-layer.tar:
Duplicate file in archive: ./etc/os-release, picking first occurrence
INFO: From ImageLayer base/static_root_amd64_debian10-layer.tar:
Duplicate file in archive: ./etc/os-release, picking first occurrence
INFO: From ImageLayer base/static_root_amd64_debian9-layer.tar:
Duplicate file in archive: ./etc/os-release, picking first occurrence
INFO: Elapsed time: 8.261s, Critical Path: 4.47s
INFO: 161 processes: 161 linux-sandbox.
INFO: Build completed successfully, 267 total actions

buildに成功しました。動作検証のため、amd64、debug、debian10のimageを作成します。

$ bazel run //experimental/ruby:debug_nonroot_amd64_debian10
INFO: Analyzed target //experimental/ruby:debug_nonroot_amd64_debian10 (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //experimental/ruby:debug_nonroot_amd64_debian10 up-to-date:
  bazel-bin/experimental/ruby/debug_nonroot_amd64_debian10-layer.tar
INFO: Elapsed time: 0.404s, Critical Path: 0.27s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
765a3652e862: Loading layer [==================================================>]  22.73MB/22.73MB
84ff92691f90: Loading layer [==================================================>]  10.24kB/10.24kB
Loaded image ID: sha256:881bad115265f80c3b74ddfc054c05958ad4c8ac0d87d9020fd0a743039a9bd2
Tagging 881bad115265f80c3b74ddfc054c05958ad4c8ac0d87d9020fd0a743039a9bd2 as bazel/experimental/ruby:debug_nonroot_amd64_debian10

$ docker image ls
REPOSITORY                 TAG                              IMAGE ID       CREATED         SIZE
bazel/experimental/ruby    debug_nonroot_amd64_debian10     881bad115265   51 years ago    44MB

$ docker run -it --rm --entrypoint=sh bazel/experimental/ruby:debug_nonroot_amd64_debian10
~ $ which ruby
/usr/bin/ruby
~ $ ls -al /usr/bin/
total 780
drwxr-xr-x    1 root     root          4096 Jan  1  1970 .
drwxr-xr-x    1 root     root          4096 Jan  1  1970 ..
-rwxr-xr-x    1 root     root          6332 Jan  1  1970 c_rehash
lrwxrwxrwx    1 root     root             6 Jan  1  1970 erb -> erb2.5
-rwxr-xr-x    1 root     root          4836 Jan  1  1970 erb2.5
lrwxrwxrwx    1 root     root             6 Jan  1  1970 gem -> gem2.5
-rwxr-xr-x    1 root     root           545 Jan  1  1970 gem2.5
lrwxrwxrwx    1 root     root             6 Jan  1  1970 irb -> irb2.5
-rwxr-xr-x    1 root     root           189 Jan  1  1970 irb2.5
-rwxr-xr-x    1 root     root        736776 Jan  1  1970 openssl
-rwxr-xr-x    1 root     root          1178 Jan  1  1970 rake
lrwxrwxrwx    1 root     root             7 Jan  1  1970 rdoc -> rdoc2.5
-rwxr-xr-x    1 root     root           937 Jan  1  1970 rdoc2.5
lrwxrwxrwx    1 root     root             5 Jan  1  1970 ri -> ri2.5
-rwxr-xr-x    1 root     root           187 Jan  1  1970 ri2.5
lrwxrwxrwx    1 root     root             7 Jan  1  1970 ruby -> ruby2.5
-rwxr-xr-x    1 root     root         14328 Jan  1  1970 ruby2.5
~ $ whoami
nonroot
~ $ ruby -v
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-linux-gnu]

$

docker imageも正しく作成されていることがわかります。やりました!成功です!

それでは試しに、このimageを試しに使ってみましょう。以下のようなsinatra applicationを動かしてみます。

require 'sinatra'

set :bind, '0.0.0.0'

get '/' do
  'Hello World!'
end

ひとまず bazel run //experimental/ruby:ruby_nonroot_amd64_debian10 でbuildした、nonrootでbusyboxのないimageをつくり、 このようなDockerfileを書いてbuildしてみます。

FROM bazel/experimental/ruby:ruby_nonroot_amd64_debian10
WORKDIR /home/nonroot
RUN ["/usr/bin/gem", "install", "--user-install", "--no-document", "sinatra"]
COPY server.rb /home/nonroot/
ENV PORT=4567
CMD ["server.rb"]
$ docker build -t distroless-ruby-sinatra-test .
[+] Building 2.7s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                                             0.0s
 => => transferring dockerfile: 264B                                                             0.0s
 => [internal] load .dockerignore                                                                0.0s
 => => transferring context: 2B                                                                  0.0s
 => [internal] load metadata for docker.io/bazel/experimental/ruby:ruby_nonroot_amd64_debian10   0.0s
 => [1/4] FROM docker.io/bazel/experimental/ruby:ruby_nonroot_amd64_debian10                     0.1s
 => [internal] load build context                                                                0.0s
 => => transferring context: 30B                                                                 0.0s
 => [2/4] WORKDIR /home/nonroot                                                                  0.0s
 => [3/4] RUN ["/usr/bin/gem", "install", "--user-install", "--no-document", "sinatra"]          2.2s
 => [4/4] COPY server.rb /home/nonroot/                                                          0.1s
 => exporting to image                                                                           0.1s
 => => exporting layers                                                                          0.1s
 => => writing image sha256:ef0b971fb3ea98f852a4e560075544e6215440eae2c6a4a75cae1044ae4788fb     0.0s
 => => naming to docker.io/library/distroless-ruby-sinatra-test                                  0.0s

$ docker run -it --rm -p 4567:4567 distroless-ruby-sinatra-test
[2021-03-21 07:57:09] INFO  WEBrick 1.4.2
[2021-03-21 07:57:09] INFO  ruby 2.5.5 (2019-03-15) [x86_64-linux-gnu]
== Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from WEBrick
[2021-03-21 07:57:09] INFO  WEBrick::HTTPServer#start: pid=1 port=4567
172.17.0.1 - - [21/Mar/2021:07:57:25 +0000] "GET / HTTP/1.1" 200 12 0.0018
172.17.0.1 - - [21/Mar/2021:07:57:25 UTC] "GET / HTTP/1.1" 200 12
- -> /
$ curl http://localhost:4567
Hello World!

このようにbuildが成功し、リクエストも受け付けるようになりました!

さて、実際の開発においてはbundlerを使用することが一般的だと思うので、このimageにbundlerを追加してみます。

https://github.com/unasuke/distroless/commit/a0de61991f750cc1a159

bundlerを追加したimageでは、このようなDockerfileを書くことでsinatraを起動させることができるようになりました。

FROM ruby:2.5-buster as build
WORKDIR /app
RUN apt update  && apt install -y imagemagick
COPY Gemfile Gemfile.lock /app/
RUN bundle install --path vendor/bundle

FROM bazel/experimental/ruby:ruby_nonroot_amd64_debian10
WORKDIR /home/nonroot
RUN ["/usr/bin/gem", "install", "--user-install", "--no-document", "sinatra"]
COPY server.rb /home/nonroot/
COPY --from=build /app/vendor /home/nonroot/vendor
COPY --chown=nonroot:nonroot Gemfile Gemfile.lock .
ENV PORT=4567
ENTRYPOINT ["/usr/bin/bundle"]
RUN ["/usr/bin/bundle", "config", "set", "path", "vendor/bundle"]
CMD ["exec", "ruby", "server.rb"]

2.3と2.5じゃない、2.7や3.0のRubyを使いたいんだけど。

2.5以降のRubyを含むimageを作成することについても作業を進めていたのですがなかなか上手くいかず、成功を待っていると記事の公開がずるずると遅れてしまうので、成功し次第別で記事を公開します。

まとめ

やってみることによって、できました。

成果については unasuke/distroless の ruby branchにpushしてあります。

https://github.com/unasuke/distroless/tree/ruby

また、この記事はZennにクロスポストしました。内容について面白かった、参考になったなどのお気持ちを「サポート」として頂けると非常に嬉しいです。非常に労力がかかっているので……

https://zenn.dev/unasuke/articles/f51fa23483bec6


  1. 2020年2月21日号 focalの開発: PHP 7.4への切り替えとPython 2の去就:Ubuntu Weekly Topics|gihyo.jp … 技術評論社 

  2. snapshot.debian.org に国内mirrorがあれば切り替えて試したかったのですが、2019年時点で90TBものストレージを必要とするらしく、なかなかmirrorも用意できないですよね 

  3. snapshot.debian.org に地理的に近いサーバーで作業をすることをおすすめします。自分が試したときはオランダにサーバーを用意してビルドを実行しました。日本からだと、このスクリプトの実行に40分前後かかるところがオランダのサーバーだと17分程度で終わりました。 

  4. https://github.com/GoogleContainerTools/distroless/pulls?q=is%3Aclosed+author%3Aapp%2Fgithub-actions 

  5. CREATEDが 51 years ago になっています。docker inspectで情報を見ると、 "1970-01-01T00:00:00Z" とepoch timeになっているので、zero fillされているものと予想できます。Jibを使って作成したdocker imageもこうなるようですね。 

  6. 依存関係は apt-rdepends をインストールして $ apt-rdepends ruby2.5 などでも調べることができます。 https://packages.debian.org/ja/buster/apt-rdepends 

Tweet
2021年03月29日
2021年02月14日

RubyとWebAssemblyの関係についてわかる範囲でまとめる

Artichoke

はじめに

2021年1月にv1.0がリリースされたWasmerにRuby Gemが存在することに触発されて調べてみました。RubyとWebAssemblyが関わっているものについてわかる範囲でまとめ、軽くどのようなものかを書いていきます。

僕自身、業務はおろかプライベートでもWASMを書いたことはなく浅い理解しかしていないですが……

WebAssembly (WASM)とは

WebAssembly は最近のウェブブラウザーで動作し、新たな機能と大幅なパフォーマンス向上を提供する新しい種類のコードです。基本的に直接記述ではなく、C、C++、Rust 等の低水準の言語にとって効果的なコンパイル対象となるように設計されています。

この機能はウェブプラットフォームにとって大きな意味を持ちます。 — ウェブ上で動作するクライアントアプリで従来は実現できなかった、ネイティブ水準の速度で複数の言語で記述されたコードをウェブ上で動作させる方法を提供します。 https://developer.mozilla.org/ja/docs/WebAssembly/Concepts

なんか、そういうやつです。

Ruby to WASM

Rubyを何らかの方法で最終的にWASM Bitecodeにコンパイルするものたちです。

blacktm/ruby-wasm

https://github.com/blacktm/ruby-wasm

紹介記事がTechRachoさんによって日本語訳されたので見覚えのある方もいるかと思います。

記事内に言及がありますが、以下のようにmrubyを経由して最終的にWASM binaryを生成します。

Ruby script → MRuby bytecode → C → emcc (Emscripten Compiler Frontend) → LLVM → Binaryen → WebAssembly

ここで登場するbinaryenですが、GitHubのWebAssembly org以下で開発されている、公式のtoolchainです。上記で行われているようなWebAssemblyへのコンパイルの他にも、wasm bitecodeからのunassemble(text formatへの変換)などの様々なツールが同梱されています。

https://github.com/WebAssembly/binaryen

Rlang

https://github.com/ljulliar/rlang

a Ruby-like language compiled to WebAssembly

Rubyのsubsetである “Rlang” からWASM bitecodeへのコンパイルを行うものです。このRlangとRubyの差異は以下にまとまっています。例えば整数型のサイズを32bitか64bitかを指定するsyntaxや、可変長引数が使用できないなどの違いがあります。

https://github.com/ljulliar/rlang/blob/master/docs/RlangManual.md

名前の由来として、C言語に変換できるSmalltalkのsubsetであるSlangからもじって付けられているようです。(そういう意味でRと名前が被っているのは仕方がないとも述べられています)

http://wiki.squeak.org/squeak/slang

prism-rb/prism

https://github.com/prism-rb/prism

Build frontend web apps with Ruby and WebAssembly

frontend web applicationをRubyとWebAssemblyで作成できるようにするframeworkです。Prism::Componentから継承したClassに記述されたapplication logicがEmscriptenによりWAMにコンパイルされて実行されるようです。

Ruby on WASM

WASM上でRubyを実行できるようにするものたちです。

Artichoke

https://www.artichokeruby.org

Bundle Ruby applications into a single Wasm executable with Artichoke, a Ruby made with Rust.

Rust実装によるRuby runtimeです。Ruby界隈ではArtichokeの名前を聞いたことのある方は多いと思います。RustはコードをWASMにコンパイルすることができるので、ArtichokeもWASMとして動かすことができます。 https://artichoke.run がPlaygroundなのですが、inspectorからWASMが動いている様子を観測できます。

Artichoke

runrb.io

ruby/rubyをEmscriptenで動かしているようです。もっと詳しく説明すると、独自patchを適用したRuby 2.6からEmscripten(emmake)でminirubyのbitecodeを生成しています。それをさらにEmscripten(emcc)でWASMにコンパイルしています。ここから先がちょっとよくわからなかったのですが、最終的にRubyをWASM bitecodeにしているのでしょうか?

https://github.com/jasoncharnes/run.rb/blob/d0f5cf9c954335795fca7c24760e728dbf47b425/src/emscripten/ruby-2.6.1/Dockerfile

PlaygroundでRubyを実行する度にworkerが生成されて別のbitecodeを実行しているようなのですが……RUBY_PLATFORMx86_64-linuxRUBY_VERSION2.6.1となっていました。Encoding.listの結果が少なかった1から実際に動いているのはminirubyなのかもしれません。

runrb.io

wruby

https://github.com/pannous/wruby

minirubyをEmscriptenでWASMにコンパイルしています。なんとmruby/mrubyをforkしています。

https://github.com/mruby/mruby/compare/master…pannous:master

mwc

https://github.com/elct9620/mwc

The tool for the developer to help them create mruby applications on the WebAssembly.

とのことですが、READMEからはイマイチどういうものかつかめなかったので、Create Projectに記載のあるmwc initが何をするか見てみたところ、<mruby.h>をincludeしてmrubyの機能を使用するC言語のコードをEmscriptenによりWASMへとコンパイルしているようでした。

https://github.com/elct9620/mwc/tree/master/lib/mwc/templates/app

mruby-L1VM

https://github.com/taisukef/mruby-L1VM

BASICが動く教育向け(でいいのか?)マイコンボードのIchigoJam上で動くmruby実装をWebAssemblyに移植したものです。mruby_l1vm.hがキモのようですね。

mruby on web - WebAssemblyのバイナリ4KB以下で動かす超軽量クライアントサイド用Ruby #ruby / 福野泰介の一日一創 / Create every day by Taisuke Fukuno

emruby

https://github.com/mame/emruby

作り込みはぜんぜんダメですが、仲間にいれてあげてください! 最近の ruby/ruby master を emscripten できるようにしてますhttps://t.co/7PvkHsZ0In

— Yusuke Endoh (@mametter) February 18, 2021

作者のmameさんから直接教えていただきました。 ruby/rubyにpatchを当てたものをEmscriptenによってWASMにコンパイルしています。(WASMにしているのはminiruby) https://mame.github.io/emruby/ で試すこともできます。最近作成されていることもあり、Rubyのversionが3.1.0devなのが凄いですね。

読み方は「いーえむるびー」であり、「えむるびー」ではないのに注意。この40行ほどのpatchでWASMにコンパイルできるんですね……

ありがとうございます!patch は、文字列を JS eval する emscripten API を生やすだけなので、なくてもコンパイルできるはずです

— Yusuke Endoh (@mametter) February 18, 2021

Emscriptenって、凄いですね。

WASM runs by Ruby

RubyからWASMを実行できるようにするものです。

wasmer.io

冒頭で紹介したものです。Wasmerは、WebAssemblyをWebブラウザ外で実行できるランタイムです。wasmer gemはそのWasmerをRubyから扱えるようにするgemとなっており、以下のドキュメントから具体的な使い方を知ることができます。

https://www.rubydoc.info/gems/wasmer/

上記ドキュメントにあるように、例えばRustのコードをWASMにコンパイルしてRubyから実行する、ということができますね。

wasmtime-ruby

https://github.com/dtcristo/wasmtime-ruby

WasmtimeのRuby bindingとあります。Wasmtimeは、(Wasmerのように)あらゆるプラットフォームでのWASMの実行を可能にすることなどを目指すBytecode Allianceのプロジェクトのひとつで、WASM及びWASIの軽量ランタイムです。

require 'wasmtime/require' とすることで、WASMバイナリ及びテキスト表現を直接 requireして実行できるのは面白いですね。

WASM.rb

https://github.com/technohippy/wasmrb

Wasmerとは異なり、pure rubyのWASM処理系です。(languageで半数以上がWASMになっていますが、これはtest dir以下に含まれているものっぽい) また、WASM binaryをRubyのhashっぽくinspectしてくれる機能もあります。

TODO.md を見る限りではまだ未実装の仕様も多いですが、WASMの勉強には(まだ)コードの量も少なく勉強するのには良さそうです。

WASM to Ruby

WASMをRubyのコードに変換するものです。(逆アセンブル?)

edvakf/wagyu

https://github.com/edvakf/wagyu

READMEには

Wagyu aims to be a library to parse and execute Web Assembly binaries on Ruby.

と書かれていますが、2019年に行われたTama Ruby会議の発表によると、WASMのテキスト表現を一度Rubyのコードへ変換してから実行するようなアプローチになっています。

WebAssemblyを Rubyにコンパイルする 黒魔術コード完全解説 - Speaker Deck

その他

「こんなものもありますよ」と教えていただいたものの、現時点でWASMは使用されていなかったものです。

webruby

https://github.com/xxuejie/webruby

Rubyではなくmrubyではありますが、Web上でruby scriptを実行できるようにするものです。 http://joshnuss.github.io/mruby-web-irb/ から実際に試すことができます。

これはEmscriptenによってmrubyをJavaScriptに変換しているのみで、WASMは使用されていませんでした。

DXOpal

まず、RubyのコードをJavaScriptに変換するOpalというコンパイラがあります。例えばRuby公式Webサイトからリンクされている、Webブラウザ上でRubyを試すことのできる https://try.ruby-lang.org ではOpalが使われています。

そして、RubyからDirextXのAPIを利用することができ、RubyによってWindows向けにゲームを開発することのできるライブラリ、DXRubyというものがあります。

DXOpalは、そのDXRubyのAPIを「だいたいそのまま」移植してWebブラウザ上でゲームを開発できるようにしたライブラリです。

そのDXOpalですが、RubyKaigi 2017にて一部をWebAssmeblyにしてみたとの発表がありました。

Ruby, Opal and WebAssembly - Speaker Deck

発表では、RubyをWebAssemblyに移植する難しさについても言及されています。

しかし発表内でデモされていたWebAssembly実装ですが、現時点ではmasterにmergeされてはいないようでした。

追記

2021-02-18

emrubyについての記述を追加しました。

2021-02-22

「その他」を追加しました。

2021-04-13

WASM.rbについての記述を追加しました。


  1. https://naruse.hateblo.jp/entry/20110118/1295345908 より 

Tweet
2021年02月14日
2021年01月17日

ElectronでPocketリーダーアプリを作っています

開発中のアプリのスクリーンショット

どんなものを作っているのか

Pocketというサービスをご存知でしょうか。あとで読みたい記事を保存しておけるサービスです。僕はこのサービスのヘビーユーザー1で、Feedリーダーからほぼ毎日何かの記事を保存し、また保存しているリストから記事を読んでいます。

この、Pocketにストックしてある記事を手軽にサクサクと読んでいきたいと思い、手の配置を固定したままどんどんと記事を読んでいけるリーダーを作成しています。(右手で記事リストを移動、左手で記事ページをスクロール)

動機

このPocketですが、利用にあたってはiOSアプリ、Androidアプリ、macOSアプリ、公式Webアプリがあるものの、Windowsには公式のアプリが存在していません。FAQでも、"Under Consideration" のままずっとステータスが変化していません。またサードパーティー製のものも見付かりません。

Webアプリを使えば済む話ではありますが、Webでは記事を都度別タブで開いて読んだら閉じてアーカイブ、というのを繰り返す必要があり、だんだんと未読消化が億劫になってきます。

macアプリのようなものがWindowsにもあれば……というのが主な開発の動機です。

また、自分のスキルとして、JavaScriptでアプリケーションをイチから構築したことがなく、そのスキルを向上させたいという思い2もありました。

構成

electron-forgeから提供されている Webpack+TypeScript templateをベースに、React、Redux、Chakra UIを使用して開発しています。どれも仕事ではあまり触れたことがなく、探り探り実装を進めています。

ベータテスター募集中

開発中のアプリのベータテスターを募集します!「あとで読む」の管理などにPocketを使っている方をお待ちしています~~~~

Pocketリーダーアプリのベータテスター募集! | うなすけ @yu_suke1994
https://t.co/EHMUPSyXft #bosyu

— うなすけ (@yu_suke1994) January 9, 2021

現時点で、本当に基本的な機能の実装は完了しています。今年中の正式公開を目指して、不具合の修正、完成度の向上を行っている途中です。あと数人ほどベータテスターを募集していますので、気になる方はよろしくお願いします。


  1. 2017年にPocketから読んだ記事数が全ユーザーのtop 1%以内に入ったというメールが来たくらいには。 

  2. Railsを主戦場としている自分が今後学ぶべき技術について(随筆) 

Tweet
2021年01月17日
2020年12月13日

Railsを主戦場としている自分が今後学ぶべき技術について(随筆)

Rails の問題は Rails のベストプラクティスがフロントエンドのベストプラクティスの邪魔になるどころか全く逆方向で相反してる点です。DHHの思想がフロントエンドと根本的に逆行してる。そういう人が作るフレームワークなのでwebpackerの抽象化を根本的に間違ったりする。

— prev.js (@mizchi) December 1, 2020

昨日もリプライで少し書いたけど、DHH自体が直近のHeyの開発でも明確にJavaScriptというものを触れないようにすることを是としているような主張をしているので、DHH wayが色濃く反映される以上この状態はもう避けられない気がしている

— potato4d / Takuma HANATANI (@potato4d) December 1, 2020

Railsがフロントエンドの最先端をゆく人々1から良く思われないのは事実として。

View層においてJavaScriptのFrameworkもしくはコードの比重が増えていくとともに、バックエンドまで一気通貫で同じ言語を使用できればいいのにという思想からか、加えてRailsとモダンフロントエンドとの相性がよくないからか、Railsの出番は今後減っていくだろうという意見に対しての賛同が増えてきているのを感じている。

blitz-js prisma rails 倒し方 - zenn.dev/mizchi

実際のところ、HTMLがサーバーから返ってきて、ちょっとしたインタラクションがある、なんていうシンプルなWebアプリケーションの世界ならば、JavaScriptの存在は「塩」であれたかもしれない2。だが、例えば自分が関わったOOPartsだったり、Webブラウザ上でネイティブアプリケーションのようなリッチな体験を提供したいとなると、RailsはよくてJSONを返すAPI serverとしての立場でしか居られず、フロントでの資産をバックエンドでも使用したいという判断になった場合にはそもそもRubyの居場所が無くなってしまう。

そんなJavaScript優勢となりつつある昨今においてのRailsの優位点のひとつに、世界最強のORM3であるところのActiveRecordの存在が挙げられるが、それも前述の記事において触れられているPrismaの出来が良くなればひっくり返る。

RESTやActive Record patternとMVCの組み合わせ、CoCなどの先見性からRailsが優れていた時代はあるにせよ、それが永遠に続くとは限らない。仮にRailsが本当に使われなくなった時代が来たとして、別に自分はRailsと(言い方が悪いが)心中するつもりはないし、そんなつもりの人は世界に存在しないんじゃないか。そうならないように、みんなContributeしている。

では、Webの世界においてRailsが「レガシー」という世界になったとき、自分はどうすればいいだろうか。学生時代にRubyを触ったのが始まりとなって新卒でRailsを書き始め、そこからキャリアの大部分にRubyとRailsがしっかりと喰らい付いている自分は、Rubyに対してもRailsに対しても結構愛着がある。レガシーとなってほしくはない。採用され続ける選択肢であってほしい。

もちろん、Railsの仕事が消えることはない4。COBOLの仕事が消えないのと同様、既存のビジネスがRailsで回っているところは、余程のことがない限りはRailsを採用し続けるだろうし、他言語、他Frameworkに移行するとなってもRailsの経験は必要になる。何より、BasecampとHeyがそのビジネスを続けていく限りは、それに合わせてRails自体も進化し続けるだろうし、文字通りの「Railsの死」というものが来年のうちに来ることはないだろう5。しかし、では2年後は?3年後は?その先は?

Railsでは食べていけない、というのは何も仕事が無くなることのみを指しているのではなく、単価の面においても「上がらない」という状況も含む。昨今はプログラミングスクールがいくつも開設されている。その指導内容に玉石混淆はありつつも、良いスクールを卒業した方々はとても優秀で、自分の実力が追い越される日もそう遠くない。そうなったとき、市場原理からいっても、Railsが書ける、というスキルだけでは単価は伸び悩む。

自分の能力について、今後何について伸ばしていくべきなのかというのを考え直す必要がある6。今後需要が増えるスキルというのが何かを考えたとき、以下のものが挙げられるだろう。

ここから書くスキルについて、「そんなのもう主流な技術じゃないか、何周遅れの議論をしているんだお前は」という感想はぐうの音も出ないほど正しい。これは単純に、Railsでしか職を得てこなかった人間の切迫感を文章にしただけである。ここに挙げられている技術で手を動かすことができていなければ、今後は厳しいだろうという勝手な焦りであって、世の技術者全員がすべからくここに挙げた技術を使いこなせているべきだとは言っていない。

2018年の段階で私が知らないこと — Overreacted

まずはTypeScript。これは先程の文脈の通り。Webアプリケーションに関わる開発者なら、既に「書けないとまずい」という域になりつつあるだろう。では、具体的に「書けないとまずい」とはどのレベルを指して言うのだろう。自分は、既存のコードベースにおいて、軽いバグ修正や、Frameworkを使用したシンプルなWebサイト作成くらいはできる。しかし、それ以上のこと、例えばいくつかのライブラリを組み合せて0からWebアプリを構築するということが苦手だ、というかできない。TypeScriptという言語は特殊で、JavaScriptへと変換しなければ実行できないので、必然的にtscやそれら変換処理、bundleを担うwebpackなどのツール群、要はエコシステムへの深い理解も求められる。まあ、bundlingのことを考えるとTypeScriptがJavaScriptだろうが、あまり関係はない。エコシステムへの習熟だって、どんな言語においてもそうだろう。

次にGo。今やすっかり開発環境に欠かすことのできないDockerなどのコンテナ技術において、第一級市民言語であるところのGoは、実行ファイルが1バイナリに収まるdeployの手軽さ、M1 chipから始まるであろうArmプロセッサのシェア拡大という状況におけるクロスコンパイルの容易性、Rubyなどスクリプト言語と比較したときの実行速度からみても、API backendの実装言語としてさらにシェアが上昇していくことが予想できる。

自分にとってGoと似た立ち位置にRustが存在する。Rustに関しては、所有権などこれまでのプログラミングには出てこなかった概念が登場することから自分で勝手に壁をつくってしまっているところがあり、非常に良くない。

“Web” という領域で体験を追求していくなら、Service WorkerとWebAssemblyも見逃す訳にはいかない。WebAssemblyへコンパイルできる高級言語としても、Go/Rustはどちらかでも実戦で手を動かすことができるようになっていると助かるだろう。

言語に関係しないスキルとしては、設計がある。自分はClean Architectureを知らぬ。ActiveRecordに頼り、密結合したMVCばかり書いてきた。conventionに頼ってきたので、いざ自由に書くとなったときに指針が無く、途方に暮れることがままある。アプリケーション単体の設計についてもそうだし、大量のアクセス、大量のデータを捌くためのアーキテクチャというものについても知識が薄い。PofEAAを読み直す時期なのかもしれない。

そういう焦燥感があるので、PodcastのサイトをNext.jsで構築したりと、プライベートの時間はTypeScriptを書くことにしている。新しく触れる技術である程度の大きさのものを作ろうとすると、その技術について必要な知識のうち、自分に不足しているものがわかってくる。自分の場合、JavaScriptの場合はasync/awaitやPromiseが絡む、非同期処理が前提となった構造のコードが全然書けない。値を取得しようとしてはawaitを付け、しかしそこはasync functionではないというエラーが出る。こういうことを幾度となく繰り返している。Goの場合はgoroutineやchannel、contextについて全く無知である。存在は知っているが、どういうものか理解できていないし書けもしない。

フリーランスという立場上、技術についていけなくなり食い扶持を失うことを強く恐れている。別に社員時代にぬるま湯に漬かっていた訳ではなく、より一層その意識が強くなったというだけのこと。そんな中、自分が主戦場としている分野が、先駆者達からはいずれ避けるべきものになるという評価にあるということを知ると、反発したい気持ちになってしまうのは当然である。しかし、例えそれがポジショントークに過ぎず、新規事業においてはまだRailsが主選択肢たりえるとしても、一旦はその主張が正しいとしたときに自分はどういう方向に進むべきかというのを考えておきたい。これはその2020年末版。

余談 モチベーション

エンジニアを仕事にしてるみんな、なにをモチベーションにして、なんのために仕事や勉強してるの?

— ダンボー田中📦 (@ktanaka117) December 9, 2020

書いている途中にこのツイートが目に入り、そういえばこの記事だと食べていけるかどうかしか気にしていなくて目的が不明確だなと思ったので自分のモチベーションについても少し書く。

自分のモチベーションは「自分が作りたいものを作れるようになりたい」で、そのために技術の幅を広げたり、仕事や勉強をしているつもりだ。ある「作りたいもの」が出てきたとき、それが実際に可能かどうかの判断、逆にある技術を知ったときに「こういう便利なものが作れるんじゃないか」という思考ができるようにしておきたい。そして実際に作れるようになりたい。

プライベートの時間に書いているというTypeScriptも、自分の日々の困り事を解決するためのアプリケーションを作成するのにJavaSctiptが適していて、ならばとTypeScriptで書いているという経緯がある。 フリーランスになってから若干、「仕事にできるかどうか」が判断基準に加わったかもしれないが、原動力は変わっていないと思っている。もちろんお金は欲しいが。


  1. 発言を引用した2人に共通する点として、「抱えているユーザー数が多い」というのがあることに書きながら気づいた。 

  2. RailsDM 2019における講演より。 https://togetter.com/li/1330578 JavaScriptは塩のようなものであるという意見。 

  3. 誰かがそう言ってたような気がするんだけどソースが見つからなかった 

  4. Railsしか書けないプログラマーのために社内にRailsプロジェクトが用意されているという「Railsが福利厚生」という状況になったら嫌だな、と思った。 

  5. とは言ったものの、DHHの考えがモダンフントエンドと乖離したままRailsが成長するのであれば、年々その開きは大きくなっていくだろうが。 

  6. いつだって考え直すべきだが? 

Tweet
2020年12月13日
古い投稿