Bazelを使ってRubyのdistroless 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を作るほうが色々と融通が効いて良いというものでしょう。ということで、作ってみます。
しかし、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が必要なので、インストールします。
上記リンクにて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を見てみます。
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
Rubyの distroless imageをbuildするためのruleを書いてみる
それでは、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 /app/vendor /home/nonroot/vendor
COPY 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
ここまでまとめるのに非常に労力がかかっているので、内容について面白かった、参考になったなどのお気持ちを「サポート」として頂けるととても嬉しいです。
-
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 ↩︎
Discussion