Rubyが動くdistroless image は作ることができるのか

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">できるかなと思ってdistrolessでRubyをやってみたら、できた。とりあえず ruby -v はできます。ruby:2.5.1-slimが178MBのところを53MBにまでできたのでサイズが半分以上減ってる。<a href="https://t.co/Me72qjgyg3">https://t.co/Me72qjgyg3</a><a href="https://twitter.com/hashtag/container_build?src=hash&amp;ref_src=twsrc%5Etfw">#container_build</a></p>&mdash; うなすけ (@yu_suke1994) <a href="https://twitter.com/yu_suke1994/status/1049685541392801793?ref_src=twsrc%5Etfw">2018年10月9日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## distroless とは
Googleが公開している、ランタイムとして使用するためのDockerイメージです。

[GoogleContainerTools/distroless: 🥑 Language focused docker images, minus the operating system.](https://github.com/GoogleContainerTools/distroless)

しかし公式で提供されている言語の中にはRubyが含まれていません。

ならやってみましょう。

### distrolessについての参考URL
- [distrolessイメージを使って、ランタイムDockerイメージを作ってみる - Qiita](https://qiita.com/some-nyan/items/90b624b0f148231748f0)
- [Base Image Journey 2018 - Speaker Deck](https://speakerdeck.com/stormcat24/base-image-journey-2018?slide=20)

## 目標
- `$ ruby -v` ができる
- sinatra でコンテナからレスポンスを返すことができる

これを目指してがんばっていきます。

## `$ ruby -v` ができるまで

突然ですが、 `ruby:2.5.3-stretch` というオフィシャルイメージにおける `/usr/local/bin/` 以下を見てみましょう。

```shell
$ docker run --rm -it ruby:2.5.3-stretch bash
root@c494cf51c146:/# ls -al /usr/local/bin/
total 188
drwxrwsr-x 1 root staff    102 Oct 18 23:33 .
drwxrwsr-x 1 root staff     84 Oct 18 23:33 ..
-rwxr-xr-x 1 root staff    605 Oct 18 23:33 bundle
-rwxr-xr-x 1 root staff    607 Oct 18 23:33 bundler
-rwxr-xr-x 1 root staff   4839 Oct 18 23:33 erb
-rwxr-xr-x 1 root staff    548 Oct 18 23:33 gem
-rwxr-xr-x 1 root staff    192 Oct 18 23:33 irb
-rwxr-xr-x 1 root staff    589 Oct 18 23:33 rake
-rwxr-xr-x 1 root staff    940 Oct 18 23:33 rdoc
-rwxr-xr-x 1 root staff    190 Oct 18 23:33 ri
-rwxr-xr-x 1 root staff 148928 Oct 18 23:33 ruby
-rwxr-xr-x 1 root staff    655 Oct 18 23:33 update_rubygems
root@c494cf51c146:/#
```

なるほど、これを雑に全部持ってくれば Ruby の動作において過不足なさそうですし、 `$ ruby -v` も動きそうです。

```dockerfile
FROM ruby:2.5.3-stretch as ruby
FROM gcr.io/distroless/base
COPY --from=ruby /usr/local/bin/ /usr/local/bin

CMD ["/usr/local/bin/ruby", "-v"]
```

```shell
$ docker build --no-cache -t distroless-ruby .
...snip...

$ docker run --rm distroless-ruby
/usr/local/bin/ruby: error while loading shared libraries: libruby.so.2.5: cannot open shared object file: No such file or directory
```

だめでした。共有ライブラリが見付からないので ruby が実行できません。

ので、共有ライブラリも持ってきます。

```dockerfile
FROM ruby:2.5.3-stretch as ruby
FROM gcr.io/distroless/base
COPY --from=ruby /usr/local/lib/ /usr/local/lib
COPY --from=ruby /usr/local/bin/ /usr/local/bin

CMD ["/usr/local/bin/ruby", "-v"]
```

```shell
$ docker build --no-cache -t distroless-ruby .
...snip...

$ docker run --rm distroless-ruby
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux]
```

よさそうですね。


## sinatra でコンテナからレスポンスを返すことができるまで
公式サイトの例にある、この単純なsinatra appを動かすことを目標とします。

```ruby:server.rb
require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello world!'
end
```

```ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"
gem "sinatra"
```

そして、このようなDockerfileを作成し、buildしてみます。

```Dockerfile:Dockerfile
FROM distroless-ruby
EXPOSE 4567
WORKDIR /app
COPY . .
RUN ["/usr/local/bin/bundle", "install"]
CMD ["/usr/local/bin/bundle", "exec", "ruby", "server.rb"]
```

`RUN` と `CMD` が exec形式[^exec]なのは、distrolessには `sh` が含まれていないために、単に `bundle install` と書くとshellが無くてエラーになるためです。

```shell
$ docker build --no-cache -t distroless-ruby-sinatra .
Sending build context to Docker daemon  2.074MB
Step 1/6 : FROM distroless-ruby
 ---> c07af2ab24f5
Step 2/6 : EXPOSE 4567
 ---> Running in 7aa0572c9251
Removing intermediate container 7aa0572c9251
 ---> 2d33d9d70ece
Step 3/6 : WORKDIR /app
 ---> Running in 0189cc6b0a87
Removing intermediate container 0189cc6b0a87
 ---> 7d922895beac
Step 4/6 : COPY . .
 ---> aa73bbf36f8e
Step 5/6 : RUN ["/usr/local/bin/bundle", "install"]
 ---> Running in acd4f58ea502
Don't run Bundler as root. Bundler can ask for sudo if it is needed, and
installing your bundle as root will break this application for all non-root
users on this machine.
/usr/local/lib/ruby/site_ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require': libz.so.1: cannot open shared object file: No such file or directory - /usr/local/lib/ruby/2.5.0/x86_64-linux/zlib.so (LoadError)
...snip...
```

ダメでしたね。`zlib` が無いので、どこかから持ってくるしかないです。(distrolessにはパッケージマネージャーがない)

### 依存するライブラリを持ってくる
`zlib` はどこにあるのでしょうか。

`ruby` のバイナリを持ってきている元、 `ruby:2.5.3-stretch` 、つまりDebian stretch で `zlib` をインストールする際には、 `apt install zlib1g` というコマンドを実行します。

[Debian -- stretch の zlib1g パッケージに関する詳細](https://packages.debian.org/stretch/zlib1g)

ならば、このときインストールされるファイルを持ってくればいいことになります。

それは `dpkg --listfiles ` で取得できるので、見てみましょう。

```shell
$ docker run --rm -it ruby:2.5.3-stretch bash
root@1ed6fdb8acb4:/# dpkg --listfiles zlib1g
/.
/lib
/lib/x86_64-linux-gnu
/lib/x86_64-linux-gnu/libz.so.1.2.8
/usr
/usr/share
/usr/share/doc
/usr/share/doc/zlib1g
/usr/share/doc/zlib1g/changelog.Debian.gz
/usr/share/doc/zlib1g/changelog.gz
/usr/share/doc/zlib1g/copyright
/lib/x86_64-linux-gnu/libz.so.1
```

実体は `/lib/x86_64-linux-gnu/libz.so.1.2.8` のようですね。これらをコピーしてきましょう。

```dockerfile:Dockerfile
FROM ruby:2.5.3-stretch as ruby
FROM gcr.io/distroless/base
COPY --from=ruby /lib/x86_64-linux-gnu/libz.so.* /lib/x86_64-linux-gnu/
COPY --from=ruby /usr/local/lib/ /usr/local/lib
COPY --from=ruby /usr/local/bin/ /usr/local/bin

CMD ["/usr/local/bin/ruby", "-v"]
```

同様に、`libyaml` も無くエラーになってしまうので、最終的に sinatraの`bundle install` が成功する distroless-ruby のDockerfileはこうなります。

```dockerfile:Dockerfile
FROM ruby:2.5.3-stretch as ruby
FROM gcr.io/distroless/base

COPY --from=ruby /lib/x86_64-linux-gnu/libz.so.* /lib/x86_64-linux-gnu/
COPY --from=ruby /usr/lib/x86_64-linux-gnu/libyaml* /usr/lib/x86_64-linux-gnu/
COPY --from=ruby /usr/local/lib/ /usr/local/lib
COPY --from=ruby /usr/local/bin/ /usr/local/bin

CMD ["/usr/local/bin/ruby", "-v"]
```

## docker run sinatra
それでは、 sinatraを動かしてみます。

```shell
$ curl -v http://localhost:4567
* Rebuilt URL to: http://localhost:4567/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 4567 (#0)
> GET / HTTP/1.1
> Host: localhost:4567
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html;charset=utf-8
< Content-Length: 12
< X-Xss-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< Server: WEBrick/1.4.2 (Ruby/2.5.3/2018-10-18)
< Date: Thu, 25 Oct 2018 15:42:46 GMT
< Connection: Keep-Alive
<
* Connection #0 to host localhost left intact
Hello world!
```


```shell
$ docker run --rm -it -p 4567:4567 distroless-ruby-sinatra
[2018-10-25 14:47:17] INFO  WEBrick 1.4.2
[2018-10-25 14:47:17] INFO  ruby 2.5.3 (2018-10-18) [x86_64-linux]
== Sinatra (v2.0.4) has taken the stage on 4567 for development with backup from WEBrick
[2018-10-25 14:47:17] INFO  WEBrick::HTTPServer#start: pid=1 port=4567
172.17.0.1 - - [25/Oct/2018:14:47:21 +0000] "GET / HTTP/1.1" 200 12 0.0158
172.17.0.1 - - [25/Oct/2018:14:47:21 UTC] "GET / HTTP/1.1" 200 12
- -> /
```

動きましたね。

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

## サイズ
```shell
$ docker image ls | grep ruby
distroless-ruby  latest                53.3MB
ruby             2.5.3-alpine3.8       45.1MB
ruby             2.5.3-slim-stretch    178MB
ruby             2.5.3-stretch         869MB
```

なるほど、alpineよりは大きいですが、slimの半分くらいにはなっていますね。

## 今後の課題
sinatraで数行程度という、軽く依存もほとんどないRuby scriptが動くようにはなりました。しかしデータベースに接続する、Railsのような大きなアプリケーションは依存関係を解決するのが難しそうです。

また、 Ruby は バッククオートでシェルコマンドを実行できる言語機能がありますが、これも動きません。busyboxなどを入れる必要がありそうです。

```ruby
`cat foo.txt`  # /bin/cat が無いので動作しない
```

[^exec]: https://docs.docker.com/engine/reference/builder/#run