うなすけとあれこれ

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 Bytecodeにコンパイルするものたちです。

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 bytecodeからの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 bytecodeへのコンパイルを行うものです。この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のbytecodeを生成しています。それをさらにEmscripten(emcc)でWASMにコンパイルしています。ここから先がちょっとよくわからなかったのですが、最終的にRubyをWASM bytecodeにしているのでしょうか?

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

PlaygroundでRubyを実行する度にworkerが生成されて別のbytecodeを実行しているようなのですが……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についての記述を追加しました。

2021-07-28

誤字を修正しました。


  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日
2020年12月12日

サーバーサイドエンジニアとして2020年に使った技術

使用したプログラミング言語統計

2020年のフロントエンドエンジニアの技術スタックの一例 | potato4d D(iary)

この記事と、TLで「これのバックエンド版が見たい」という発言に触発されたので書いてみます。口語体と文語体が入り乱れてるのは許してください。

冒頭のグラフはwakatimeで生成した今年1年間のプログラミング言語使用率です。2位はTypeScript、3位はTerraform、4位はYAMLでした。

立場

フリーランスで、主にRailsやAWSを使用しているサービスの運用、開発に関わっています。いくつもの会社を見てきた訳ではなく、数社に深く関わっている1都合上、視野が狭いかもしれません。

公開している成果としては クラウドゲーミング最新開発事例 - #CEDEC2020 - Speaker Deck があります。

長年RubyとRailsを書いてきたので、技術スタックがそのあたりに偏っています。

利用した技術一覧

太字は特記事項があるものです。また、挙げている技術は「ある程度触った」くらいで記載しており、全てに習熟している訳ではありません2

各項目について、上のほうがよく使っているものという傾向がありますが、厳密ではないです。

主軸としてはRuby/Rails/AWSの主要コンポーネント というところでした。

特記事項

Ruby

今年も僕の主戦場はRubyでした。あと数年はRuby以外を軸に仕事をしていくことになるんじゃないかな……?ただ2020年らしいことは全くしていなくて、例えばRuby 2.7で導入された機能、Pattern matchingやnumberd parameterは使った記憶がないです。(Pattern matchingは使おうとしたが、愚直に書いたほうが短かくなったので断念)

TypeScript

今年はJavaScriptをBrowser console以外で書いた記憶がありません。

Rails

キャリアがRailsから始まっているので、思い入れがあります。また、新しい現場でもすぐにコードを書けるところは助かっています。とは言えコードが肥大化してくると何がどうやって動いているのか把握するのには時間がかかりますが、これはまあどんなものでもそうでしょう。 業界においてRailsが優位にあるとすれば、その実装速度の早さと、コードやデータ量、ビジネスロジックがある程度成長しても耐えられるRubyの表現力及びActiveRecordの底力があるように思います。あと数年は新規実装としてのRailsの立場は残っていると思います。それ以降はまた状況が変わっていると思うのでわかりません。

React/Vue.js

これを1つにまとめたのは、主にRails appに組み込まれたものを書いていたためです。TypeScriptもそうなのですが、JavaScriptに関しては既存のコードベースに手を入れるということはできても、設計を含めた0から書き始めるということができていない、できないのが自分の弱みだと感じているので、プライベートの時間ではキャッチアップをしています。

Docker

コンテナ技術はもう前提条件と言えるくらいには利用が広がっていますね。ECSやKubernetesをはじめとしたエコシステムの発展もあり、開発環境から本番環境までDockerで環境が統一されていることの便利さ、docker-compose一発で開発環境が準備できる手軽さは一度体験するともう戻れないと言っていいです。

Docker for Macが遅い遅いと言われ続けている昨今ですが、僕の環境においては許容範囲というか、あまりそこで困らなかった印象があります。大分改善されているのではないでしょうか。それとも僕が鈍いのか。

Fargate/ECS

業務としては一切Kubernetesを使用しませんでした。関わっている範囲だとこの2つで十分要求を満たすことができていました。

IntelliJ Idea系

これは、今年春から JetBrains All Products Pack を契約して使い始めました。これについては1本ブログ記事を書くつもりで下書きを温めていたのですが、この際ですからそれから引用する形にします。

Rubyを第一言語としていますが、PythonやGoなどで書かれたAPI serverの開発を素早く完了させる必然性が増えました。 これまでは腰を据えてプロダクトの全容をあらかた把握した上で開発を進めることが多かったのですが、とにかく素早く問題点を発見して修正する、それもあまり習熟していない言語で、という状況に置かれることが増えてきました。 それに、習熟しているつもりのRubyですら、今までにない大規模なアプリケーションの機能開発においては全容を把握しておくということが困難です。

また、DB clientとしてのDataGripが非常に便利なのは嬉しい誤算でした。

おわりに

あまりRails界隈でこういう記事をみかけない気がするので、他の皆さんはどうなのか知りたいので書いてほしかったり……年の瀬に1年を振り返ってみるのは楽しかったです。


  1. おおっぴらに言わないだけで探せばわかります 

  2. インターネット・強・パーソンの皆さまは、あれがないこれがないこいつはショボいとお叩きになられるのでは……とビクビクしています。 

Tweet
2020年12月12日
2020年12月02日

ナ組Minecraftサーバーの監視について —マイクラサーバー監視2020—

はじめに

皆さん、Minecraftしてますか。サーバー、立ててますか。監視、してますか?

この記事では、2020年10月末に爆誕したMinecraftサーバー「ナ組サーバー」について、僕が勝手に監視している方法について現時点での構成をまとめておくものです。

なんか突然GCPを触りたくなったのでナ組マイクラ鯖を立てた

— 蜘蛛糸まな🕸️ / HolyGrail (@HolyGrail) October 30, 2020

注意

「マイクラサーバー監視2020」と題していますが、僕はこれまでにMinecraftのサーバーを運用した経験はありません。何ならここで言及するサーバーについても、構築したのは蜘蛛糸まな氏です。 単純に、今Minecraftサーバーの監視をするならどうするか、ということについて述べています。過去のベストプラクティスは知りません。

ナ組サーバーとは

ナナメさん (@7name_) とそのお友達が遊んでいるMinecraftサーバーです。構築は前述の通り、蜘蛛糸まな氏です。GCP上に構築されています。

#ナ組マイクラ - Twitter検索 / Twitter

この動画の冒頭でも成り立ちについては述べられています。

僕は、このサーバーへのアクセス権を頂いているので、そこから色々な作業を行い監視環境を構築してみました。以下は行なったことについてのまとめとなります。

監視ツールについて

まず、サーバーの監視を行うときに「何でモニタリングするか」というのが大きな部分を占めるでしょう。SaaSであればMackerelやDatadog、GCP上のサーバーであればCloud Monitoringを使っておくのもありでしょう。自分で構築するのであれば、PrometheusやZabbixやNagiosなどの選択肢もあります。

今回は、Grafanaとの連携で見た目が良いこと、新しめのものであることから、Prometheusで監視、Grafanaで可視化という構成を採用しています。

MinecraftへのPluginの導入

先ほどPrometheusを導入することに決めた、と書きましたが、そもそもMinecraftサーバーの状態をPrometheusから取得することができなければ無意味です。

Minecraftに導入できるPrometheus exporterはいつくか存在します。

star数、ドキュメントの量から、 sladkoff/minecraft-prometheus-exporter を導入することに決めました。このようなexporterが存在していたことも、Prometheus採用の一因です。

MinecraftサーバーへのPluginの導入については、各サーバーによってまちまちだと思うので詳しくは触れませんが、今回は特定のディレクトリ以下へplugin本体を展開しておくだけで済みました。

plugin自体の設定はYAMLで記述することができるので楽ですね。いい感じにやっておきましょう。

Prometheus, Grafana serverの構築

今回、MinecraftとPrometheus、及びGrafanaは同居させず、別のサーバーに分離することにしました。どちらもワンバイナリで動作するので、サーバーの適当なディレクトリに置いて起動させるだけで構築は大体完了です。追加でやることは、Prometheusのscraping_configsにMinecraftサーバーを追加で指定することと、Grafanaのdata sourceにPrometheusを追加すること程度です。

Webサーバー運用についてのHTTPS化などのエトセトラはここで詳細に記述することはしません。1

dashboardの作成

ここまででGrafanaを使用してダッシュボードを作成することができるようになっていはずなので、あとはチマチマと必要そうなグラフを並べたダッシュボードを作成します。ここで作成したダッシュボードの変化を見ているときが一番楽しいですね。

ダッシュボード

このダッシュボードについてですが、MinecraftサーバーのIPアドレスがわかってしまうため、一般に公開はしていません。2

アラートの設定

サーバーというものは、いつ止まってもおかしくありませんし、止まってしまっているときには何らかの方法で通知が欲しいものです。

Prometheusによるアラートといえば、Alertmanagerがあります。Alertmanagerは様々な手段でアラートを送信することができますが、一番手軽なものはWebhookによる通知なのではないでしょうか。そこで、Webhookによる通知をAlertmanagerから送信しようとしてみましたが、これがDiscordのWebhookには対応していませんでした。3

Alertmanager integration with Discord · Issue #1365 · prometheus/alertmanager

ナ組はコミュニケーションをDiscord上で行なっているので、そこに通知を集約できないのはつらいです。

しかし幸いなことに、Grafana自体のArert機能においては、DiscordのWebhookをサポートしていることが判明しました。

https://grafana.com/docs/grafana/latest/alerting/notifications/#list-of-supported-notifiers

そして、指定した閾値に達した場合に次のようにアラートをWebhook経由でDiscordに飛ばせることが確認できました。

テストアラートの様子

ここでは、Minecraftが使用しているJVMのThread数が異常な値になった場合に通知をするという単純なルールになっています。

まとめとアドベントカレンダーについて

このようにしてナ組のMinecraftサーバーは監視されています。Minecraftは10年近く遊ばれているゲームであり、インターネット上の情報も豊富ではあるものの古くなっている記述が沢山あります。監視に関する情報も、Prometheusを使用しているような比較的新しいものは見当らなかったので、こうやって記事にしてみました。

また、この記事は whywaita Advent Calendar 2020 の12/2を担当する記事となります。whywaitaさんは現在プライベートクラウドに関する仕事に関わっていたり、過去にPrometheus clusterを破壊するなどの輝かしい功績のあることですから、今回僕の行なった作業についても、より良い方法を知っているものと思います。一度、対面で教えていただきたいものですね。


  1. certbotを使用しています。それ以外の詳細については面倒になったので書きません。 

  2. わからないようにできないか?とがんばってみましたが僕には無理でした。 

  3. 代替手段として https://github.com/benjojo/alertmanager-discord がありますが、コンポーネントを増やすことになるので導入はしませんでした。 

Tweet
2020年12月02日
2020年11月30日

このブログをS3 Static website hosting + CloudFrontからAWS Amplifyに移行しました

amplify console

2017年からS3 Static website hostingとCloudFrontの組み合わせで運用してきたこのブログを、AWS Amplifyによる運用へと変更しました。

理由

S3 syncが遅い

記事、画像が増えてくると、S3 syncに時間がかかるようになってきます。大体10分前後かかるということで、ちょっとなんとかしたいなと思っていました。また、deployに使用している middleman-s3_sync gem の開発が停滞しているように見えるのも脱S3 syncの要因のひとつです。良く言えば安定している、と言えなくもないし、嫌ならS3 syncによるdeployをやめればいいという話ではあるかもしれませんが……

管理が楽

大した管理をしていたわけでもないですが、S3とCloudFrontの2つのサービスによる構成から、Amplifyだけを見れば良くなるので少し楽になります。

裏側の構成は変化しない

Amplifyも裏はCloudFront+S3っぽいし、何度か計測してると逆転することもあったので有意差はなさそう https://t.co/wa6hgje5PH

— うなすけ (@yu_suke1994) July 24, 2020

という感じで、Amplify consoleでもCloudFrontのdistrinution URLがCNAMEとして指定する値と出てくるように、CloudFrontがリクエストを受けるという構成は変化しないことがわかっていました。なので大した影響はないだろうと判断しました。

ハマり

ダウンタイムが発生する

元々のCloudFrontで握っているTLS証明書との兼ね合いか、一度既存のCloudFrontのdistributionを削除(無効化ではだめだった)してからでないとAmplify console側で以前のものと同じドメインを設定させることができないようです。

Switching Cloudfront+S3 => Amplify with custom domain requires downtime · Issue #22 · aws-amplify/amplify-console

そのためにダウンタイムは必ず発生してしまいます。

それがどのくらいかかるかについてですが、ダウンタイムは体感では5分もなかったように思います。

(特に予告なくシレッと移行作業を行いました)

Tweet
2020年11月30日
2020年10月17日

Kaigi on Rails STAY HOME Edition 配信の裏側

配信構成

御礼

Kaigi on Railsという初めてのイベントに参加していただいた皆様、素晴らしい発表をしてくださった発表者の皆様、惜しくも採択とはいかなかったものの、Proposalを提出してくださった皆様、協賛していただいたスポンサーの皆様、本当にありがとうございます。

Kaigi on RailsではProposal提出まわりと、yrindaさんと一緒に配信・動画まわりを担当していたうなすけです。

https://kaigionrails.org/team/

この記事では、完全オンライン開催にあたっての配信まわりについて、裏側がどうだったかについて触れるものです。リアル開催からオンライン開催に切り替えた苦労話なんかはチーフオーガナイザー氏が書いてくれたのでここでは触れません。

Kaigi on Railsのチーフオーガナイザーを務めました - okuramasafumiのブログ

はじめに本番構成

全てをすっ飛ばして、本番の配信環境をまず紹介します。

本番配信はメインのyrinda houseとサブのunasuke houseの2系統を用意して冗長化していました。メインで何か問題が発生したらサブ側の配信URLを広報し、そちらを視聴してもらうイメージでした。そのために2人が同じ内容をメインとサブで配信する必要があり、シーンの切り替えタイミング等は当日全員が入っているDiscordでタイミングを合わせて行っていました。

ライブ発表の場合の構成は、アプリケーション側のZoomを発表者の映像用に、モニターを1枚占有した状態のブラウザからZoomに入り発表資料用に、そしてこれらをOBSに流しこみました。 その他に、それらの画面に重ねる発表情報の枠、Zoomからの音声出力キャプチャなどの設定をライブ発表用に用意しました。

配信構成図

動画発表の場合は、発表する方々から事前に提出いただいた動画を、同様の見た目になるよう発表用の枠をかぶせて書き出し、当日は再生するのみの運用になるようにしました。

当日のタイムテーブルの進行については、発表者、休憩それぞれでOBSのシーンを作成し、基本的にはそれを上から切り替えていくという省エネ運用になるように配信担当側で協力して作り込んでいきました。

OBS scene構成

それまでのストーリー

まず当初、Kaigi on Railsはリアル会場で開催するつもりでした。ところが時世が変化し、どうしてもリアルで集まっての開催ができなくなり、他の多くのイベントと同様にインターネット上での配信による開催に切り替えることとなりました。

オンライン開催となったとき、ではどのような形式で開催するのかということが問題になってきます。配信プラットフォームはどれにするのか、どのように配信を行うのか、日付、時間帯、各トークの長さ、配信を委託するのか自分達でやるのか……

何度か話し合いを行った結果、運営チームにある程度の設備があることから、配信は自分達で行うことに、プラットフォームは強い決め手がある訳ではないですが、YouTube Liveを選択しました。議事録を振り返っていたのですが、全部録画もしくは配信ではなく、録画と配信を希望制にした理由は残っていませんでした。議論するまでもなく、そう決まったのだと思います。

オンラインイベントにすると決まった訳ですが、我々には知見が全くありませんでした。そこで、Kaigi on Railsそのものの宣伝も兼ね、「Kaigi on Rails new」というイベントを開催し、そこでオンラインイベントの “感じ” を掴むことにしました。

Kaigi on Rails new - Kaigi on Rails | Doorkeeper

結果様々なKPTを洗い出すことができ、本番までに何を準備すればよいか、どういう運用にすればいいのかが見えてきました。

その後は提出してもらった動画を編集し、書き出したものを共有したりで動画の準備は進んでいきます。機材がない方のために、事前に録画会をやろうという企画もありましたが、希望者少数のため実現しませんでした。

配信勢の皆さんに対しては、直前に配信テスト会を開催してZoomでの登壇の感じをつかんでもらう一方、配信スタッフ側でも実際にどういうオペレーションになるかの細部を詰めました。具体的には登壇者交代がどうなるか、などです。

そして、前述のような構成になりました。

当日起こったトラブル

反省点

音量のバラつき

事前に頂いていた動画の音量は正規化していたのですが、配信側の音量が想定以上に大きく、相対的に動画のほうが小さく聞こえてしまう問題がありました。アーカイブ公開版では再度正規化を行って、できる限りバラつきがないようにしてあります。

そもそもの配信構成について

振り返ってみると、配信にあたって回線を冗長化するよりは、オペレーターを冗長構成にすべきだったかもしれません。要するに配信環境を1箇所に構築し、そこに配信担当者が集合するという体制です。今回は何事もなく終わりましたし、何事があってもイベントが進行するように準備をしましたが、次回どうするかはまた考える必要があります。何事もそうですが。

参考にした情報

今後

Kaigi on Railsは来年も開催することが決定となっていますが、「やる」以外は何も決まっていません(本当に、何も……)。配信環境についても、今回の反省を活かして何かしらの改善を行っていきたいですね。例えばクラウド上からの配信にしたり、配信用機材を導入したりといったことが考えられます。1年かけて、ゆっくり考えていきたいです。


この記事はHHKB Professional HYBRID Type-SとHelixPicoとChoco60で書かれました。

Tweet
2020年10月17日
新しい投稿
古い投稿