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は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.py
と Dockerfile
は、そのままでは動きません。
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のようなscript言語は、実行時に必要となるライブラリ群をまとめて1つないし複数個のバイナリとして固めることができません。がんばれば実行時に必要なファイルを列挙することもできるでしょうが、その作業と実際の配置を行うのは困難です。
そのような労力を惜しまず、上記のようなBazel ruleを記述してdistroless imageを使うのか、それともslim imageでやっていくのかは、distrolessにする作業のコストと、image sizeとattack surfaceを減らすことにより得られるメリットを比較して判断することになると思います。
そして多くの場合において、slim imageを使うことが最適解になるのではないかとも思っています。
……あと、そもそもexperimental扱いですし。
この記事はZennにクロスポストしました。内容について面白かった、参考になったなどのお気持ちを「サポート」として頂けると非常に嬉しいです。
https://zenn.dev/unasuke/articles/5ee6e2067ab1ba
軽量なイメージのベースとして使われることも多い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に単純に置き換えてもいいかどうかはしっかり検証を行ったうえで実行する必要があるでしょう。
Javaや.Netについては知識が不足しているためなんとも言えません ↩
元々、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についても初心者なので誤っている点があると思います。そのような点がありましたら、僕に連絡するかご自身で訂正する記事を公開してもらえたら嬉しいです。
Bazelとdistrolessのbuildにあたっては /usr/bin/env python
が2系である必要があるので、手元の環境をそのようにします。新しめのubuntuでは $ sudo apt install python-is-python2
でそのようになります1。
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
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で実行できる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に分かれて必要なファイルがガッチャンコされているんだな、ということがわかります。
さて、container imageのbuildが成功したところで、今buildしたcc imageのbuild ruleを見てみます。
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には libgcc1
と libgomp1
と libstdc++6
が含まれていることも予想できます。DISTRO_PACKAGES
と DISTRO_SUFFIX
は base/distro.bzl から、 ARCHITECTURES
は checksums.bzl から来ていることもわかります。
ccについてはそれほど行数もないこともあり、大体の処理を把握できました。それではdistrolessなRubyを作成するにあたり、近いことをしていると予想できるPython3の場合を見てみます。147行と少し長いので、ここぞ!と思われる部分を抜き出します。
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
それでは、Python3のbuild ruleを参考にして、Rubyのものを書いてみます。
debian10(buster)とdebian9(stretch)において、Rubyをインストールするための情報は以下に記載されています。
ここから、ruby2.5
(debian10の場合) をインストールするために必要な依存パッケージを全て列挙6し、そのうちまだ記載されていないものを WORKSPACE
の dpkg_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_image
の debs
に列挙し、その他諸々を整えます。
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.5以降のRubyを含むimageを作成することについても作業を進めていたのですがなかなか上手くいかず、成功を待っていると記事の公開がずるずると遅れてしまうので、成功し次第別で記事を公開します。
やってみることによって、できました。
成果については unasuke/distroless
の ruby branchにpushしてあります。
https://github.com/unasuke/distroless/tree/ruby
また、この記事はZennにクロスポストしました。内容について面白かった、参考になったなどのお気持ちを「サポート」として頂けると非常に嬉しいです。非常に労力がかかっているので……
https://zenn.dev/unasuke/articles/f51fa23483bec6
2020年2月21日号 focalの開発: PHP 7.4への切り替えとPython 2の去就:Ubuntu Weekly Topics|gihyo.jp … 技術評論社 ↩
snapshot.debian.org
に国内mirrorがあれば切り替えて試したかったのですが、2019年時点で90TBものストレージを必要とするらしく、なかなかmirrorも用意できないですよね ↩
snapshot.debian.org
に地理的に近いサーバーで作業をすることをおすすめします。自分が試したときはオランダにサーバーを用意してビルドを実行しました。日本からだと、このスクリプトの実行に40分前後かかるところがオランダのサーバーだと17分程度で終わりました。 ↩
https://github.com/GoogleContainerTools/distroless/pulls?q=is%3Aclosed+author%3Aapp%2Fgithub-actions ↩
CREATEDが 51 years ago
になっています。docker inspectで情報を見ると、 "1970-01-01T00:00:00Z"
とepoch timeになっているので、zero fillされているものと予想できます。Jibを使って作成したdocker imageもこうなるようですね。 ↩
依存関係は apt-rdepends
をインストールして $ apt-rdepends ruby2.5
などでも調べることができます。 https://packages.debian.org/ja/buster/apt-rdepends ↩
2021年1月にv1.0がリリースされたWasmerにRuby Gemが存在することに触発されて調べてみました。RubyとWebAssemblyが関わっているものについてわかる範囲でまとめ、軽くどのようなものかを書いていきます。
僕自身、業務はおろかプライベートでもWASMを書いたことはなく浅い理解しかしていないですが……
WebAssembly は最近のウェブブラウザーで動作し、新たな機能と大幅なパフォーマンス向上を提供する新しい種類のコードです。基本的に直接記述ではなく、C、C++、Rust 等の低水準の言語にとって効果的なコンパイル対象となるように設計されています。
この機能はウェブプラットフォームにとって大きな意味を持ちます。 — ウェブ上で動作するクライアントアプリで従来は実現できなかった、ネイティブ水準の速度で複数の言語で記述されたコードをウェブ上で動作させる方法を提供します。 https://developer.mozilla.org/ja/docs/WebAssembly/Concepts
なんか、そういうやつです。
Rubyを何らかの方法で最終的にWASM Bytecodeにコンパイルするものたちです。
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
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
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にコンパイルされて実行されるようです。
WASM上でRubyを実行できるようにするものたちです。
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が動いている様子を観測できます。
ruby/rubyをEmscriptenで動かしているようです。もっと詳しく説明すると、独自patchを適用したRuby 2.6からEmscripten(emmake)でminirubyのbytecodeを生成しています。それをさらにEmscripten(emcc)でWASMにコンパイルしています。ここから先がちょっとよくわからなかったのですが、最終的にRubyをWASM bytecodeにしているのでしょうか?
PlaygroundでRubyを実行する度にworkerが生成されて別のbytecodeを実行しているようなのですが……RUBY_PLATFORM
は x86_64-linux
、RUBY_VERSION
は 2.6.1
となっていました。Encoding.list
の結果が少なかった1から実際に動いているのはminirubyなのかもしれません。
https://github.com/pannous/wruby
minirubyをEmscriptenでWASMにコンパイルしています。なんとmruby/mrubyをforkしています。
https://github.com/mruby/mruby/compare/master…pannous:master
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
https://github.com/taisukef/mruby-L1VM
BASICが動く教育向け(でいいのか?)マイコンボードのIchigoJam上で動くmruby実装をWebAssemblyに移植したものです。mruby_l1vm.h
がキモのようですね。
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って、凄いですね。
2022年1月19日に、kateinoigakukunさんによってCRubyのplatformとしてWASIを対象としたコンパイルができるようになりました。kateinoigakukunさんは1月14日にMatzのapproveによってRuby commiterになられています。すごい……
これは2021年度Rubyアソシエーション開発助成によって支援されている取り組みでもあります。
2021年度Rubyアソシエーション開発助成金 公募結果発表
RubyプログラムをWebAssemblyバイナリにパッケージ化し、1バイナリでの配布を可能にすることも目指す
ともあります。とても期待が高まりますね。
実際にどういう風に試せるかについては、公式のドキュメント及びznzさんの記事が参考になります。
RubyからWASMを実行できるようにするものです。
冒頭で紹介したものです。Wasmerは、WebAssemblyをWebブラウザ外で実行できるランタイムです。wasmer gemはそのWasmerをRubyから扱えるようにするgemとなっており、以下のドキュメントから具体的な使い方を知ることができます。
https://www.rubydoc.info/gems/wasmer/
上記ドキュメントにあるように、例えばRustのコードをWASMにコンパイルしてRubyから実行する、ということができますね。
https://github.com/dtcristo/wasmtime-ruby
WasmtimeのRuby bindingとあります。Wasmtimeは、(Wasmerのように)あらゆるプラットフォームでのWASMの実行を可能にすることなどを目指すBytecode Allianceのプロジェクトのひとつで、WASM及びWASIの軽量ランタイムです。
require 'wasmtime/require'
とすることで、WASMバイナリ及びテキスト表現を直接 require
して実行できるのは面白いですね。
https://github.com/technohippy/wasmrb
Wasmerとは異なり、pure rubyのWASM処理系です。(languageで半数以上がWASMになっていますが、これはtest dir以下に含まれているものっぽい) また、WASM binaryをRubyのhashっぽくinspectしてくれる機能もあります。
TODO.md を見る限りではまだ未実装の仕様も多いですが、WASMの勉強には(まだ)コードの量も少なく勉強するのには良さそうです。
WASMをRubyのコードに変換するものです。(逆アセンブル?)
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は使用されていなかったものです。
https://github.com/xxuejie/webruby
Rubyではなくmrubyではありますが、Web上でruby scriptを実行できるようにするものです。 http://joshnuss.github.io/mruby-web-irb/ から実際に試すことができます。
これはEmscriptenによってmrubyをJavaScriptに変換しているのみで、WASMは使用されていませんでした。
まず、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されてはいないようでした。
emrubyについての記述を追加しました。
「その他」を追加しました。
WASM.rbについての記述を追加しました。
誤字を修正しました。
Ruby本体についての記述を追加しました。
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を使っている方をお待ちしています~~~~
— うなすけ (@yu_suke1994) January 9, 2021
Pocketリーダーアプリのベータテスター募集! | うなすけ @yu_suke1994
https://t.co/EHMUPSyXft #bosyu
現時点で、本当に基本的な機能の実装は完了しています。今年中の正式公開を目指して、不具合の修正、完成度の向上を行っている途中です。あと数人ほどベータテスターを募集していますので、気になる方はよろしくお願いします。
2017年にPocketから読んだ記事数が全ユーザーのtop 1%以内に入ったというメールが来たくらいには。 ↩
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で書いているという経緯がある。 フリーランスになってから若干、「仕事にできるかどうか」が判断基準に加わったかもしれないが、原動力は変わっていないと思っている。もちろんお金は欲しいが。
発言を引用した2人に共通する点として、「抱えているユーザー数が多い」というのがあることに書きながら気づいた。 ↩
RailsDM 2019における講演より。 https://togetter.com/li/1330578 JavaScriptは塩のようなものであるという意見。 ↩
誰かがそう言ってたような気がするんだけどソースが見つからなかった ↩
Railsしか書けないプログラマーのために社内にRailsプロジェクトが用意されているという「Railsが福利厚生」という状況になったら嫌だな、と思った。 ↩
とは言ったものの、DHHの考えがモダンフントエンドと乖離したままRailsが成長するのであれば、年々その開きは大きくなっていくだろうが。 ↩
いつだって考え直すべきだが? ↩
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以外を軸に仕事をしていくことになるんじゃないかな……?ただ2020年らしいことは全くしていなくて、例えばRuby 2.7で導入された機能、Pattern matchingやnumberd parameterは使った記憶がないです。(Pattern matchingは使おうとしたが、愚直に書いたほうが短かくなったので断念)
今年はJavaScriptをBrowser console以外で書いた記憶がありません。
キャリアがRailsから始まっているので、思い入れがあります。また、新しい現場でもすぐにコードを書けるところは助かっています。とは言えコードが肥大化してくると何がどうやって動いているのか把握するのには時間がかかりますが、これはまあどんなものでもそうでしょう。 業界においてRailsが優位にあるとすれば、その実装速度の早さと、コードやデータ量、ビジネスロジックがある程度成長しても耐えられるRubyの表現力及びActiveRecordの底力があるように思います。あと数年は新規実装としてのRailsの立場は残っていると思います。それ以降はまた状況が変わっていると思うのでわかりません。
これを1つにまとめたのは、主にRails appに組み込まれたものを書いていたためです。TypeScriptもそうなのですが、JavaScriptに関しては既存のコードベースに手を入れるということはできても、設計を含めた0から書き始めるということができていない、できないのが自分の弱みだと感じているので、プライベートの時間ではキャッチアップをしています。
コンテナ技術はもう前提条件と言えるくらいには利用が広がっていますね。ECSやKubernetesをはじめとしたエコシステムの発展もあり、開発環境から本番環境までDockerで環境が統一されていることの便利さ、docker-compose一発で開発環境が準備できる手軽さは一度体験するともう戻れないと言っていいです。
Docker for Macが遅い遅いと言われ続けている昨今ですが、僕の環境においては許容範囲というか、あまりそこで困らなかった印象があります。大分改善されているのではないでしょうか。それとも僕が鈍いのか。
業務としては一切Kubernetesを使用しませんでした。関わっている範囲だとこの2つで十分要求を満たすことができていました。
これは、今年春から JetBrains All Products Pack を契約して使い始めました。これについては1本ブログ記事を書くつもりで下書きを温めていたのですが、この際ですからそれから引用する形にします。
Rubyを第一言語としていますが、PythonやGoなどで書かれたAPI serverの開発を素早く完了させる必然性が増えました。 これまでは腰を据えてプロダクトの全容をあらかた把握した上で開発を進めることが多かったのですが、とにかく素早く問題点を発見して修正する、それもあまり習熟していない言語で、という状況に置かれることが増えてきました。 それに、習熟しているつもりのRubyですら、今までにない大規模なアプリケーションの機能開発においては全容を把握しておくということが困難です。
また、DB clientとしてのDataGripが非常に便利なのは嬉しい誤算でした。
あまりRails界隈でこういう記事をみかけない気がするので、他の皆さんはどうなのか知りたいので書いてほしかったり……年の瀬に1年を振り返ってみるのは楽しかったです。
皆さん、Minecraftしてますか。サーバー、立ててますか。監視、してますか?
この記事では、2020年10月末に爆誕したMinecraftサーバー「ナ組サーバー」について、僕が勝手に監視している方法について現時点での構成をまとめておくものです。
なんか突然GCPを触りたくなったのでナ組マイクラ鯖を立てた
— 蜘蛛糸まな🕸️ / HolyGrail (@HolyGrail) October 30, 2020
「マイクラサーバー監視2020」と題していますが、僕はこれまでにMinecraftのサーバーを運用した経験はありません。何ならここで言及するサーバーについても、構築したのは蜘蛛糸まな氏です。 単純に、今Minecraftサーバーの監視をするならどうするか、ということについて述べています。過去のベストプラクティスは知りません。
ナナメさん (@7name_) とそのお友達が遊んでいるMinecraftサーバーです。構築は前述の通り、蜘蛛糸まな氏です。GCP上に構築されています。
この動画の冒頭でも成り立ちについては述べられています。
僕は、このサーバーへのアクセス権を頂いているので、そこから色々な作業を行い監視環境を構築してみました。以下は行なったことについてのまとめとなります。
まず、サーバーの監視を行うときに「何でモニタリングするか」というのが大きな部分を占めるでしょう。SaaSであればMackerelやDatadog、GCP上のサーバーであればCloud Monitoringを使っておくのもありでしょう。自分で構築するのであれば、PrometheusやZabbixやNagiosなどの選択肢もあります。
今回は、Grafanaとの連携で見た目が良いこと、新しめのものであることから、Prometheusで監視、Grafanaで可視化という構成を採用しています。
先ほどPrometheusを導入することに決めた、と書きましたが、そもそもMinecraftサーバーの状態をPrometheusから取得することができなければ無意味です。
Minecraftに導入できるPrometheus exporterはいつくか存在します。
star数、ドキュメントの量から、 sladkoff/minecraft-prometheus-exporter を導入することに決めました。このようなexporterが存在していたことも、Prometheus採用の一因です。
MinecraftサーバーへのPluginの導入については、各サーバーによってまちまちだと思うので詳しくは触れませんが、今回は特定のディレクトリ以下へplugin本体を展開しておくだけで済みました。
plugin自体の設定はYAMLで記述することができるので楽ですね。いい感じにやっておきましょう。
今回、MinecraftとPrometheus、及びGrafanaは同居させず、別のサーバーに分離することにしました。どちらもワンバイナリで動作するので、サーバーの適当なディレクトリに置いて起動させるだけで構築は大体完了です。追加でやることは、Prometheusのscraping_configs
にMinecraftサーバーを追加で指定することと、Grafanaのdata sourceにPrometheusを追加すること程度です。
Webサーバー運用についてのHTTPS化などのエトセトラはここで詳細に記述することはしません。1
ここまでで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を破壊するなどの輝かしい功績のあることですから、今回僕の行なった作業についても、より良い方法を知っているものと思います。一度、対面で教えていただきたいものですね。
certbotを使用しています。それ以外の詳細については面倒になったので書きません。 ↩
わからないようにできないか?とがんばってみましたが僕には無理でした。 ↩
代替手段として https://github.com/benjojo/alertmanager-discord がありますが、コンポーネントを増やすことになるので導入はしませんでした。 ↩
2017年からS3 Static website hostingとCloudFrontの組み合わせで運用してきたこのブログを、AWS Amplifyによる運用へと変更しました。
記事、画像が増えてくると、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側で以前のものと同じドメインを設定させることができないようです。
そのためにダウンタイムは必ず発生してしまいます。
それがどのくらいかかるかについてですが、ダウンタイムは体感では5分もなかったように思います。
(特に予告なくシレッと移行作業を行いました)
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のシーンを作成し、基本的にはそれを上から切り替えていくという省エネ運用になるように配信担当側で協力して作り込んでいきました。
まず当初、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で書かれました。
タイトルに書いてあるように、travisの org から com に移行した、という話なのですが、そこに至るまでに様々な苦労をしたのでお願いだから最後まで読んでねぎらってください。
ItamaeのCIには、travis-ci.org を使用していました。
CIというものは、たまに落ちることがあります。その原因は内部のコードが悪い場合や外部要因である場合などが挙げられます。ItamaeのCIも、例によってたまに落ちることがありました。
たまに落ちることはよくて、落ちたCIを再度実行して通れば良いのです。しかし問題は、「落ちたtestを再度実行することができない」 というところにありました。
いつからなのかは不明ですが、travis-ci.org では失敗したtestを再実行するUIが消えてしまっており、空commitを積むなどしなければ同じコードでのtestの再実行ができなくなっていました。
この “More Options” 内の “Requests” は一見再buildのリクエストのように見えますがそのような挙動はしません。
これは非常につらい。ので、CIをtravis-ci.orgではない別の何かに乗り換えることにしました。
CIについて話す前に、Itamaeのtestにおける対象の組み合わせについて触れておきます。CIでは以下のような組み合わせに対してテストを行っています。
unitなのかintegrationなのか、というのは、unitはItamae gemの実装に対してテストを行い、integrationは用意されているrecipeをDocker containerに適用して意図した状態にできているかを確認するテストとなっています。
(以前はDigitalOceanで起動したインスタンスに対してrecipeの適用を行っていました)
さて、OSSなら無料で使用できるCIサービスというものはいくつかありますが、代表的なものに以下の2つが上げられると思います。
新しもの好きということもあり、まずGitHub Actionsを試してみることにしました。
まずGitHub Actionsでのunit testは以下のようなYAMLで実行できます。
name: "unit test on ubuntu"
on:
push:
branches: "*"
jobs:
test:
strategy:
matrix:
os: [ubuntu-16.04, ubuntu-18.04]
ruby: [2.2, 2.3, 2.4, 2.5, 2.6, 2.7, head]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- run: bundle install
- run: bundle exec rake spec:unit
簡単ですね。これで問題なくunit testはpassするようになります。
しかし、GitHub Actionsでは integration testがどうしても成功しないという問題に直面してしまいました。
なぜ通らないのかの原因がわかればいいのですが、これが全くの不明でした。上記画像にあるように落ちているものもあれば通っているものもあり、全体的に不安定になっています。落ちている原因そのものはtmpディレクトリに書き込みができなくなって落ちているのですが、なぜそうなっているのかはわかりません。
友人に相談したところ、興味深い事実が明らかになりました。
「straceをonにするとpassする」 のです。
straceは何をするツールなのかと言うと、システムコールをトレースするツールです。それを有効にしただけで通るようになる、つまり実行に対してオーバーヘッドがかかるとtmpへの書き込みが成功してtestが成功する、という現象が発生しているようなのです。
なぜこのような状況になっているのかの調査はちょっとハードルが高すぎるので、GitHub Actionsの採用は見送ることにしました。
(self-hosted runnerを使用することでGitHub Actionsで使用されているインスタンスに特有の問題なのかを調査することもできますが、そこまでの元気は出ませんでした)
次に手を出したのはCircleCIです。CircleCIでは最近matrix jobを記述できるようになり、複数の条件を組み合わせたテストが書けるようになりました。
https://circleci.com/blog/circleci-matrix-jobs
とはいえ複雑なYAMLになってしまいました。
version: 2.1
orbs:
ruby: circleci/ruby@1.0.7
executors:
docker:
docker:
- image: cimg/base:stable
docker-1804:
docker:
- image: cimg/base:stable-18.04
machine:
machine:
image: circleci/classic:201808-01
jobs:
unit:
parameters:
ruby-version:
type: string
exec:
type: executor
default: ""
executor: << parameters.exec >>
steps:
- checkout
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run: ruby -v
- run: bundle exec rake spec:unit
integration:
parameters:
ruby-version:
type: string
executor: machine
# executor: docker
steps:
- checkout
- setup_remote_docker:
version: 18.06.0-ce
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run:
command: |
ruby -v
export PATH=$HOME/.rvm/bin:$PATH
ruby -v
- run: bundle exec rake spec:integration:all
unit-jit:
parameters:
ruby-version:
type: string
executor: docker
steps:
- checkout
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run: ruby -v
- run: RUBYOPT=--jit bundle exec rake spec:unit
integration-jit:
parameters:
ruby-version:
type: string
# executor: machine
executor: docker
steps:
- checkout
- setup_remote_docker:
version: 18.06.0-ce
- ruby/install:
version: << parameters.ruby-version >>
- run: gem install bundler --version 1.17.3 --force
- run: bundle install -j4
- run: ruby -v
- run: RUBYOPT=--jit bundle exec rake spec:integration:all
workflows:
version: 2
all-test:
jobs:
- unit:
exec:
name: docker-1804
matrix:
parameters:
ruby-version: ["2.3"]
- unit:
exec:
name: docker
matrix:
parameters:
ruby-version: ["2.4", "2.5", "2.6", "2.7"]
- integration:
matrix:
parameters:
ruby-version: ["2.3", "2.4", "2.5", "2.6", "2.7"]
# all-test-with-jit:
# jobs:
- unit-jit:
matrix:
parameters:
ruby-version: ["2.6", "2.7"]
- integration-jit:
matrix:
parameters:
ruby-version: ["2.6", "2.7"]
CircleCIでItamaeのtestを実行するにあたり、いくつかの壁に突き当たったので紹介します。
CircleCIにはOrbという仕組みがあり、汎用的な手順ならYAMLに記述しなくてもOrbを導入することによって記述を省略できる仕組みがあります。
Rubyの実行環境を用意するOrbとして、CircleCIが公式で用意しているのが circleci/ruby
です。
このOrbですが、bundlerをインストールするのにGemfile.lockの存在をアテにしている部分があります。
Itamaeはgemであり、Gemfile.lockをリポジトリに含んではいません。ではこのOrbは使えないのかというとそうではなく、よく読むとわかるように bundler-version
を渡すとGemfile.lockがなくても指定したversionのbundlerをインストールできるように見えます。
しかし既存のbundlerを上書いてしまってよいかの確認ダイアログが出るため、どちらにしろ上手く動きません。
結局、bundlerのinstallは手で --force
を付与したコマンドを実行させることで回避しました。
unit testはこれでうまくいくようになりましたが、dockerコマンドを使用する都合上、machine executorで実行しているintegration testでエラーが出るようになりました。それもJITを有効にしている場合のみ失敗します。
エラーを見ると、--jit
というオプションが不正というものでした。しかし、このスクリーンショットで使用しているRubyは2.6であり、--jit
は有効なはずです。
不審に思い、Rubyのversionも出力するようにしたのが以下のスクリーンショットです。
なんと、Ruby 2.6 をインストールしたはずなのに、使用されているのはRuby 2.3になっています。これはRVMのPATHをこねくりまわしてもどうしても解決することができませんでした。どうしてこんなことになるのでしょう。
二進も三進もいかないので、remote dockerを使用してみることにしました。これは、unit testで使用しているDocker executorであればRubyのversionが正しく設定できているので、その環境においてdockerコマンドを使用できるようにするための仕組みです。
デプロイする Docker イメージを作成するには、セキュリティのために各ビルドに独立した環境を作成する特別な setup_remote_docker キーを使用する必要があります。 この環境はリモートで、完全に隔離され、Docker コマンドを実行するように構成されています。 ジョブで docker または docker-compose のコマンドが必要な場合は、.circleci/config.yml に setup_remote_docker ステップを追加します。
しかし、やはりエラーになってしまいます。これはテストの過程においてItamaeのコードをまるっとdocker container側にvolume mountしている部分があるのですが、remote dockerがvolume mountをサポートしていないためにエラーになります。
ジョブ空間からリモート Docker 内のコンテナにボリュームをマウントすること (およびその逆) はできません。 https://circleci.com/docs/ja/2.0/building-docker-images/#section=configuration
(docker cpはできますが)
という様々な躓きがあり、CircleCIを使うのはあきらめようと考えました。 (あきらめるまでに60回以上CIを回しています)
ではどこのCIを使おうか、となるのですが、そもそもtravis-ci.comへの統合が進められていることに気付きました。
2018年に、Travis CIはGitHub Appsとして導入できるようになり、OSSも travis-ci.com
でCIを実行できるようになっています。
2020年の今でも travis-ci.org
を使用しているので、試しに travis-ci.com
に移行してみることにしました。
この移行はとても簡単で、 travis-ci.com
にGitHub accountでログインしてMigrateボタンをクリックするだけです。
結果ですが、このように “Restart build” ボタンが出現しており、テストの再実行ができるようになりました!
という諸々があり、ItamaeのCIは travis-ci.com
に移行することになります。
これについて、第53回情報科学若手の会のLT枠で発表した資料を貼っておきます。