ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

archof: リモートのままイメージのarchitectureを確認するコマンド

背景

Apple SiliconのMacが販売されてそれなりの時間も経ち、エコシステムもできてきてお仕事の開発に使っている人も増えてきたように思う。

ところで、colimaのようなツールを使い、Docker環境をMacに作成した時、Apple Silicon上ではデフォルトで arm64 版のDocker*1がインストールされる。

$ docker info | grep Archi
 Architecture: aarch64

この上で何も考えずにイメージをビルドしたら arm64 のイメージができる。

この時、このイメージをリモートのレジストリGCRGHCRなどを想像してほしい)にpushにしたら、もちろん arm64 のイメージがpushされる。

ここで、このイメージをx86_64の本番サーバにpullして走らせるような運用だったら、困ったことになるだろう...。

このような違うアーキテクチャのイメージをうっかりプッシュしてしまう事故は、意外と防ぐ仕組みが簡単ではない*2

さらに言うと、pushされたイメージのアーキテクチャがなんであるかを確認するのも意外と骨が折れる。pullしてしまえば docker inspect なりで確認すればすぐわかるが、何度もプッシュしていてそのうちいくつかに混ざってしまったような場合とか、そもそもイメージが大きい場合とかは難しいこともありそう。

と言うことで Docker Registory API を叩いてリモートから確認するコマンドを作った。

github.com

インストール

$ go install github.com/udzura/archof

Go 1.17 で動作確認しています。多分1.18などでも問題なさそう。

様子

$ archof docker.io/amd64/ubuntu:latest
amd64

$ archof docker.io/arm64v8/ubuntu:latest 
arm64

認証が必要なやつはBearer認証に対応しているので、たとえばGCRだとこう言う感じで使えます。

$ archof gcr.io/udzura-dev/sample:latest --bearer "$(gcloud auth print-access-token)"                                                                     
amd64

内部

マニフェストを取得して、そこからConfigのDigestを取得してBlobを取得すればいいんですが、今回利用した go-containerregisrty はBlobの方のAPIをどう叩けばいいかよくわからず、URLを自分で構築して直接叩くことになった。いい方法があるんだろうか。

ref, _ := name.ParseReference(target)
desc, _ := remote.Get(ref, remote.WithAuth(&authn.Bearer{Token: token}))

reg := ref.Context()
url := fmt.Sprintf("%s://%s/v2/%s/blobs/%s",
    reg.Scheme(),
    reg.RegistryStr(),
    reg.RepositoryStr(),
    sha,
)
res, _ := desc.Client.Get(url)

type BlobResponse struct {
    Architecture string `json:"architecture"`
}
b := BlobResponse{}

if err := json.NewDecoder(res.Body).Decode(&b); err != nil {
    panic(err)
}

fmt.Println(b.Architecture)

雑感

Go の思い出しも兼ねて作ったが、特に変わったことをしていないので思い出せたのかどうか。

あとコマンドの名前の主語が大きいかも?(対象はコンテナイメージだけなのに) と思ったが、覚えやすいしえいやと付けました。ご了承ください。

とりあえず不安になった時にお使いください。というかpush防ぐ方法ってないんですかね...。

*1:正確にはLinuxVMイメージ

*2:いい方法があれば知りたい、というのもこのブログを書いたモチベーションの一つ

How to build ruby.wasm on Apple Silicon Mac

ここまでのあらすじ

<読み飛ばし推奨>

子どもたちのためにJava版マインクラフトを買い、よーしパパ*1MOD作って「威厳*2」を見せちゃうぞ〜 と思ったが全くやる気配がない。

一方で、子どもは無限の時間をScratchに費やしていた。Scratch 3.0はReactで作られていることが知られており、いわゆるSPA(もしかして、この用語もう古い?)であり、基本的にはGitHub Pagesのようなバックエンドレスな環境にもデプロイできる。

また、TypeScript/JavaScript+Reactで拡張を書くことが出来る。自作の拡張は残念ながら公式のScratchでは利用できないが、自分でビルドしてデプロイしたScratchインスタンスで使うことができる。一種のMODである。例えば機械学習に触れることが出来る拡張を有効にしたMODが知られている。

champierre.github.io

ということで、サーバサイドやカーネルHackをいくらやっても子どもたちに相手をしてもらうのは難しい*3ため、フロントエンドをやっていくことにした。


しかしフロントエンド、ここまでのキャリアにおいてほぼ、全く触れていない、せいぜいVueとReactのチュートリアルをなぞってTODOアプリをマスターした程度のレベルだし、それも忘れてしまった。

ひとまずとっつきやすいところから始めよう、ならWebAssemblyだな、なんかRubyも対応したし、と思ったのでそこからやることにした。前途多難であることは言うまでもない。

www.ruby-lang.org

</読み飛ばし推奨>

要するにRuby 3.2 preview1 のソースコードから WASM 版Rubyをビルドすることにした。

必要な情報は以下のリポジトリにある。

github.com

が、ここでLinux環境であればDockerfileが提供され、どう言うツールが必要でどうインストールするかが全部まとまっており、このイメージを作ればビルドできる...

はずなのだが見れば分かるとおり x86_64 環境のみを想定している。これは関連ツールがLinuxの場合x86_64のバイナリしかないとか、色々仕方ない面がある。すなわちApple Siliconの上で生活してる人の場合、Linux環境、ことDockerイメージを作ってその上で作業というのは逆に難しい。

この辺は以前znzさんが試した際にも言及されている。

blog.n-z.jp

今回はmacOS + Apple Siliconな環境でのビルド手順をまとめる。


ひとまず今回必要なツールキットは以下のような感じである。

順番にインストール方法を解説する。

wasi-sdk

公式に配られているバイナリ、Mac版はアーキテクチャが明示されておらず、不安なので自分でビルドする。

github.com

ビルドの依存は cmake、clang、ninja らしい。いかついが、clangはXCodeのアレで入ってるはずだし、cmakeとninjaはHomebrewなどで入れれば問題ない。

あとはチェックアウトして以下でOK。

git checkout wasi-sdk-14
NINJA_FLAGS=-v make package

build/wasi-sdk-14.0 に関連ファイルが作られるので ~/wasi など適当なディレクトリにコピーする。

cp -vr build/wasi-sdk-14.0/* ~/wasi/

Binaryen

wasi-sdkはWASIのSDKか! って感じだが、Binaryen が何者か調べてみても難しい...。

こんな感じだと理解した。

こちらはarm64のMac向けバイナリがある。

wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-arm64-macos.tar.gz
tar xzf binaryen-version_105-arm64-macos.tar.gz
cp -vr binaryen-version_105/* ~/wasi/

ruby.wasm

いよいよruby.wasmリポジトリに何があるかの話になる*4

github.com

Emscriptenで作ったやつとWASIの上に作るやつがあるらしく、WASI向けの情報は このディレクトリ にまとまっているようだが、ひとまず rake コマンドで色々ビルドできる。

$ rake -T
rake build:head-wasm32-unknown-emscripten-full        # Build head-wasm32-unknown-emscripten-full
rake build:head-wasm32-unknown-emscripten-minimal     # Build head-wasm32-unknown-emscripten-minimal
rake build:head-wasm32-unknown-wasi-full              # Build head-wasm32-unknown-wasi-full
rake build:head-wasm32-unknown-wasi-full-debug        # Build head-wasm32-unknown-wasi-full-debug
rake build:head-wasm32-unknown-wasi-full-js           # Build head-wasm32-unknown-wasi-full-js
rake build:head-wasm32-unknown-wasi-full-js-debug     # Build head-wasm32-unknown-wasi-full-js-debug
rake build:head-wasm32-unknown-wasi-minimal           # Build head-wasm32-unknown-wasi-minimal
rake build:head-wasm32-unknown-wasi-minimal-debug     # Build head-wasm32-unknown-wasi-minimal-debug
rake build:head-wasm32-unknown-wasi-minimal-js        # Build head-wasm32-unknown-wasi-minimal-js
rake build:head-wasm32-unknown-wasi-minimal-js-debug  # Build head-wasm32-unknown-wasi-minimal-js-debug
rake deps:libyaml-wasm32-unknown-emscripten           # build libyaml 0.2.5 for wasm32-unknown-emscripten
rake deps:libyaml-wasm32-unknown-wasi                 # build libyaml 0.2.5 for wasm32-unknown-wasi
rake fetch_artifacts[run_id]                          # Fetch artifacts of a run of GitHub Actions
rake npm:all                                          # Build all npm packages
rake npm:configure_prerelease[prerel]                 # Configure for pre-release
rake npm:ruby-head-wasm-emscripten                    # Build npm package ruby-head-wasm-emscripten
rake npm:ruby-head-wasm-emscripten-check              # Check npm package ruby-head-wasm-emscripten
rake npm:ruby-head-wasm-wasi                          # Build npm package ruby-head-wasm-wasi
rake npm:ruby-head-wasm-wasi-check                    # Check npm package ruby-head-wasm-wasi
rake publish[tag]                                     # Publish artifacts as a GitHub Release
rake wapm:irb-build                                   # Build wapm package irb
rake wapm:irb-publish                                 # Publish wapm package irb
rake wapm:ruby-build                                  # Build wapm package ruby
rake wapm:ruby-publish                                # Publish wapm package ruby

wasm32-unknown みたいなのはRustのターゲット名に合わせた呼び方だと思う。で、suffixがいくつかあって

  • minimal: 拡張(Cコンパイルが必要ということだと思う) standard library 抜きでビルドする(like json, yaml, or stringio など)
  • full: 拡張ライブラリ含め全部ビルドする
  • *-js: JavaScript で使いやすいような形でビルドし、Glue的なファイルも用意する。npmパッケージにするみたい
  • *-debug: DWARFの情報や、WASMのnameセクション? を含む。明らかにバイナリサイズがデカくなりそう。

今回はとにかくシンプルに rake build:head-wasm32-unknown-wasi-minimal を試す。通常のRubyのビルドよろしくconfigureから走る。

$ rake build:head-wasm32-unknown-wasi-minimal
warning: vfs feature is not enabled due to no LIB_WASI_VFS_A
/opt/ghq/github.com/ruby/ruby.wasm/build/src/head/configure --host wasm32-unknown-wasi --build arm64-apple-darwin21 --with-static-linked-ext --with-ext="" --with-libyaml-d
ir="/opt/ghq/github.com/ruby/ruby.wasm/build/deps/wasm32-unknown-wasi/opt/libyaml/usr/local" LDFLAGS="-Xlinker -zstack-size=16777216" XLDFLAGS="/opt/ghq/github.com/ruby/ru
by.wasm/build/ext-build/head-wasm32-unknown-wasi-minimal/extinit.o" debugflags="-g0" --disable-install-doc
checking for ruby... /Users/udzura/.rbenv/versions/3.1.1/bin/ruby
tool/config.guess already exists                                                                                                                                           tool/config.sub already exists
checking build system type... aarch64-apple-darwin21
checking host system type... wasm32-unknown-wasi
checking target system type... wasm32-unknown-wasi
checking for wasm32-unknown-wasi-wasm-opt... no
checking for wasm-opt... wasm-opt
configure: WARNING: using cross tools not prefixed with host triplet
checking wheather $WASI_SDK_PATH is set... yes
checking for wasm32-unknown-wasi-cl.exe... /Users/udzura/wasi/bin/clang
checking for /Users/udzura/wasi/bin/llvm-ar... /Users/udzura/wasi/bin/llvm-ar
checking for /Users/udzura/wasi/bin/clang++... no
checking for /Users/udzura/wasi/bin/llvm-nm... no
...

WASMなRubyrubies ディレクトリにできる。

$ file rubies/head-wasm32-unknown-wasi-minimal/work-wasm/usr/local/bin/ruby 
rubies/head-wasm32-unknown-wasi-minimal/work-wasm/usr/local/bin/ruby: WebAssembly (wasm) binary module version 0x1 (MVP)
$ cd rubies/head-wasm32-unknown-wasi-minimal/work-wasm/usr/local/bin
$ wasmtime ./ruby -- -e 'p RUBY_VERSION'
`RubyGems' were not loaded.
`error_highlight' was not loaded.
`did_you_mean' was not loaded.
"3.2.0"

という感じ。この先は... あまり考えていない...。

*1:このコピペがもう古い

*2:本当は全く威厳を示すつもりはなく、単に共通の話題が欲しいという話だが、単語の響きが面白いのでここで使ってしまった

*3:諸説あります

*4:READMEとエスパーで書いた内容なので間違っていたら優しく指摘してください

Apple Silicon上でx86_64のDockerを使う

Apple Silicon上でLinux向けx86_64の環境が欲しくなったり、x86_64向けのバイナリやイメージを作らないといけなくなる事態は、稀に良くあると思う。

colimaを使うと、Aarch64なMac上であっても、かなり楽にx86_64のLinux環境とDocker環境が手に入る。

最近のcolimaには実は --arch というオプションがあり、x86_64又はaarch64であればあっさりと環境を作ることができるようになっている。

デフォルトで入ってくる環境と区別するため --profile オプションを明示する必要がある。

$ colima --profile colima-x64 start --arch x86_64

colimaの作ったDockerインスタンスcolima list で一覧できる。

$ colima list
PROFILE       STATUS     ARCH       CPUS    MEMORY    DISK
default       Running    aarch64    2       2GiB      60GiB
colima-x64    Running    x86_64     2       2GiB      60GiB

--profile を明示して colima start を実行すれば、ホストの docker コマンドでアクセスするインスタンスを切り替えられる*1。defaultに戻すときはオプションなしで colima start を打つ。

$ colima --profile colima-x64 start
$ docker info
Client: 
 Context:    colima-colima-x64
 Debug Mode: false

Server:
...
 OSType: linux
 Architecture: x86_64
...

実際どれくらい使えるの?

とは言え、これはAarch64の上でQEMUを経由してx86_64のCPUをエミュレートしているわけで、パフォーマンスがどれくらい出るかは気になる。

以下の条件で、Goのプロジェクトのコンパイル時間を比較してみた。

環境

ホスト: macOS Monterey 12.2.1(21D62)、Apple M1 Max(10コア)、64GB memory ゲスト: x86_64/aarch64共に、 CPU 2 、メモリ 2GiB (colimaが作成するインスタンス設定のデフォルト)

ビルドの方法

ビルドする対象としてはある程度の規模がある、ほぼ純粋なGoのプロジェクトが良いため、 gopher-lua を選んだ。

github.com

まず、apt等によるネットワーク状況の影響を最小限にするため、以下のDockerfileからbaseイメージを作成し、

FROM golang:1.18-bullseye

RUN apt -y update && apt -y install git make python
RUN mkdir /build && git clone https://github.com/yuin/gopher-lua /build/gopher-lua

これを元に以下のDockerfileのビルドが完了する時間を測定した*2

FROM go-build-bench-base

WORKDIR /build/gopher-lua
RUN make glua

CMD ["file", "/build/gopher-lua/glua"]

結果

x86_64 on Apple Silicon

$ docker build --no-cache -t go-build-bench .
[+] Building 14.9s (7/7) FINISHED                                                                              
 => [internal] load build definition from Dockerfile                                                      0.0s
 => => transferring dockerfile: 37B                                                                       0.0s
 => [internal] load .dockerignore                                                                         0.0s
 => => transferring context: 2B                                                                           0.0s
 => [internal] load metadata for docker.io/library/go-build-bench-base:latest                             0.0s
 => [1/3] FROM docker.io/library/go-build-bench-base                                                      0.0s
 => CACHED [2/3] WORKDIR /build/gopher-lua                                                                0.0s
 => [3/3] RUN make glua                                                                                  14.1s
 => exporting to image                                                                                    0.4s 
 => => exporting layers                                                                                   0.4s 
 => => writing image sha256:1b34681df8e9a8db531586e52a37f2d39e5db52de3acb19d500838f6e5b6f166              0.0s 
 => => naming to docker.io/library/go-build-bench                                                         0.0s

aarch64 on Apple Silicon

$ docker build --no-cache -t go-build-bench .
[+] Building 1.5s (7/7) FINISHED                                                                               
 => [internal] load build definition from Dockerfile                                                      0.0s
 => => transferring dockerfile: 37B                                                                       0.0s
 => [internal] load .dockerignore                                                                         0.0s
 => => transferring context: 2B                                                                           0.0s
 => [internal] load metadata for docker.io/library/go-build-bench-base:latest                             0.0s
 => [1/3] FROM docker.io/library/go-build-bench-base                                                      0.0s
 => CACHED [2/3] WORKDIR /build/gopher-lua                                                                0.0s
 => [3/3] RUN make glua                                                                                   1.4s
 => exporting to image                                                                                    0.0s
 => => exporting layers                                                                                   0.0s
 => => writing image sha256:5f5da0ae77cb42e8c0378b73f964c10b662bfd7f4988c13c474c54c961590db3              0.0s
 => => naming to docker.io/library/go-build-bench                                                         0.0s

結構歴然と差が...。

10倍近い差がついてしまうため、Goなどクロスコンパイル環境が整っているのならaarch64の上でx86_64のバイナリを作る方が速い*3。例えば以下のようにDockerfileを変えてビルドしたときの結果を残す。

FROM go-build-bench-base

WORKDIR /build/gopher-lua
ENV GOARCH amd64
RUN make glua

CMD ["file", "/build/gopher-lua/glua"]
$ docker build --no-cache -t go-build-bench .
[+] Building 4.4s (7/7) FINISHED                                                                               
 => [internal] load build definition from Dockerfile                                                      0.0s
 => => transferring dockerfile: 194B                                                                      0.0s
 => [internal] load .dockerignore                                                                         0.0s
 => => transferring context: 2B                                                                           0.0s
 => [internal] load metadata for docker.io/library/go-build-bench-base:latest                             0.0s
 => [1/3] FROM docker.io/library/go-build-bench-base                                                      0.0s
 => CACHED [2/3] WORKDIR /build/gopher-lua                                                                0.0s
 => [3/3] RUN make glua                                                                                   4.3s
 => exporting to image                                                                                    0.1s 
 => => exporting layers                                                                                   0.1s 
 => => writing image sha256:7bc11d676123ead4f72b6d3b34d0580571b14ae751d2b4e9f71005babac59df4              0.0s 
 => => naming to docker.io/library/go-build-bench                                                         0.0s 

基本的に「本番バイナリはGitHub Actionで/Cloud Buildで/他〜」という運用にして確実かつ高速にx64サーバ上でビルドした方がよさそう。

が、そうは言っても手元で作れた方がいい... とか、色々な非常事態的場面もあるだろう、ということで残しておく。


追記

BuildX について教えてもらった。クロスプラットフォームではこっちを使う方が良さそう。いずれにせよCPUエミュレーションなどのオーバヘッドはかかりそうだが試したい。

docs.docker.com

追記2

あと、はてブにあった通り、Rosetta 2 ( arch コマンド)経由で仮想環境を作れるのならばそちらも試したいが、やり方が見つからずという感じなので、有識者に教えてほしい。

*1:両方インスタンスが立ち上がっていればすぐに切り替わる

*2:とはいえgo getの速度などはネットワーク状況で変わりうるので、あくまでも大体

*3:バイナリ単体であればこれで対応できるけど、「イメージ作りたいっす」だと困っちゃうね...

Notion の使い方、私見など

1月がまるっとスキップされている... だと...。


hisaichi5518.hatenablog.jp

id:hisaichi5518 さんがこういうことを書いていて、非常にわかりみがあった。

一方で、Notionというツールはそこまですごい銀の弾丸ではないというか、得意な分野と無理に使っても旨味がない分野/工夫しないと使えない分野が結構極端にあるような気がしていて、とりあえず僕は僕で雑な感想を残しておこうと思った。

個人の日記レベルですが。

続きを読む

プラグインを書いて学ぶGNU NSS

Linuxアドベントカレンダー13日目の月曜日の記事です。

qiita.com

今日は以前から気になっていたNSS pluginの仕様について調べてまとめ、簡単なものを書いてみようと思います。というのも、実は 同僚が 二人も NSS pluginを開発して公開しており、ずっと負けてられないなと思っていました。

GNU Name Service Switch (NSS) とは

さて、OSを使っていると、名前の正体が何者なのか、あるいはこのIDは人間がどういう名前をつけているのか、それぞれ確認する機会が頻繁にあります。

Linuxの場合、その「人間が使う名前と、OSが使うID」の相互解決をName Service Switchというものがやってくれます*1

*1:FreeBSDなどにもほぼ同じ仕組みがありますが、今回はLinuxのやつを取り上げると言うことで

続きを読む