AWS Lambdaで自前のコンテナイメージを動かす場合は/etc/hostsに注意
まとめ
- AWS Lambdaでコンテナイメージを使う場合、使うイメージと動かすコードによっては
No such file or directory @ rb_sysopen - /etc/hosts
エラーになる可能性がある - 自前でイメージをビルドしていて、かつコードが
/etc/hosts
を読むような動きをしていると上記のエラーが発生する(基本的に/etc/hosts
はイメージに含まれないため) - AWSが提供する https://gallery.ecr.aws/lambda/ruby のイメージを使う場合は問題ない(
/etc/hosts
が含まれている) - 回避策としては、Dockerfile内でCOPYを使い空のhostsファイルをイメージに追加する方法がある
事件発生
筆者が所属しているReproには、AWSのStepFunctionからRailsのコンテナイメージをLambda Functionとして呼び出し、Rakeタスクを実行するという実装がある( Ruby on Rails on Lambda – Speaker Deck )。
そのLambda Functionがある日突然動かなくなった。エラーログを見ると、 /etc/hosts
を読もうとしているがファイルが存在していない。どういうことだろうか?もちろん、昨日までは元気に動いていたし、関連するコードを触った形跡はない。
/opt/ruby/lib/ruby/3.0.0/resolv.rb:192:in `initialize': No such file or directory @ rb_sysopen - /etc/hosts (Errno::ENOENT)
from /opt/ruby/lib/ruby/3.0.0/resolv.rb:192:in `open'
from /opt/ruby/lib/ruby/3.0.0/resolv.rb:192:in `block in lazy_initialize'
暫定対処
そもそもなんで /etc/hosts
を読もうとしているのか、スタックトレースを見てみると Resolve#getaddress
を読んでいた。
Resolv.getaddress(HOST)
これはデフォルトだとhostsファイルとDNSを参照しているため、 /etc/hosts
を読もうとしていたというわけだ。ひとまず、暫定対処としてResolvが /etc/hosts
を読まないように initializerで制御することとした。
if ENV["AWS_EXECUTION_ENV"]&.start_with?("AWS_Lambda_")
Resolv::DefaultResolver.replace_resolvers([Resolv::DNS.new])
end
これでDNSのみ使うようになるので、ファイルがなくてエラーになることはなくなる。
原因を探る
暫定対処が完了したので、なぜ /etc/hosts
がなくなったのかを調べる。Reproではコンテナイメージのビルド用にEC2インスタンスを用意していて、このトラブルの前にビルド用インスタンスを入れ替えていた。タイミングとしてはこれが怪しい、ということで旧ビルドサーバーと新ビルドサーバーでイメージを作って比較すると、確かに旧ビルドサーバーでビルドしたイメージには /etc/hosts
ファイルが含まれるが新ビルドサーバーでビルドしたイメージには含まれない。
では、新旧の違いは何だろうかと考えるとき、一番の可能性として挙げられるのはdockerのバージョンだろう。
旧ビルドサーバーは結構昔に作られたもので、dockerのバージョンは 19.03.13-ce
。新ビルドサーバーは 20.10.13
と差があることがわかった。
Server:
Engine:
Version: 19.03.13-ce
API version: 1.40 (minimum version 1.12)
Go version: go1.13.15
Git commit: 4484c46
Built: Mon Oct 12 18:51:50 2020
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: 1.4.1
GitCommit: c623d1b36f09f8ef6536a057bd658b3aa8632828
runc:
Version: 1.0.0-rc92
GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Server:
Engine:
Version: 20.10.13
API version: 1.41 (minimum version 1.12)
Go version: go1.16.15
Git commit: 906f57f
Built: Thu Mar 31 19:21:13 2022
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: 1.4.13
GitCommit: 9cc61520f4cd876b86e77edfeb88fbcd536d1f9d
runc:
Version: 1.0.3
GitCommit: f46b6ba2c9314cfc8caae24a32ec5fe9ef1059fe
docker-init:
Version: 0.19.0
GitCommit: de40ad0
また、最小構成で再現させるためのDockerfileを検証したところ、以下のように何かRUNが一つでもあると再現することがわかった。
# syntax = docker/dockerfile:experimental
FROM debian:bullseye
RUN ls -la
さらに、DOCKER_BUILDKIT=1
などでBuildkitを使うと再現することもわかった。つまり、docker 19.03.13-ce
が使うBuildkitとdocker 20.10.13
が使うBuildkitで挙動が違うのではないかと推測できる。
それぞれが使うBuildkitのバージョンを調べると、旧ビルドサーバーでは v0.6.4-32-gdf89d4dc
を、新ビルドサーバでは v0.8.3-4-gbc07b2b8
を使っていた。
github.com/moby/buildkit df89d4dcf73ce414cd76837bfb0e9a0cc0ef3386 # v0.6.4-32-gdf89d4dc
https://github.com/moby/moby/blob/v19.03.13/vendor.conf#L29
github.com/moby/buildkit bc07b2b81b1c6a62d29981ac564b16a15ce2bfa7 # v0.8.3-4-gbc07b2b8
https://github.com/moby/moby/blob/v20.10.13/vendor.conf#L36
このバージョン間の差分を調べると、0.8.0で入ったclear file mount stubs and fix empty layer cases by tonistiigi · Pull Request #1739 · moby/buildkit が原因であることがわかった。このPullRequestより前だとRUNステップが一つでも実行されると空の /etc/hosts
ファイルがイメージに残っていたようだ。このPullRequestによってRUN ステップの最後に /etc/hosts
が削除されるようになっており、それによりコンテナイメージに含まれなくなっていた、ということだった。
/etc/hosts を含んだコンテナイメージを作る
なるほど、原因はわかった。ではどうするのがよいか。暫定対処で行ったResolverの置き換えは認識していないと意図せぬ挙動につながりそうなので、最後の手段としたい。そうするとlambda function実行時に /etc/hosts
を作るか、コンテナイメージに /etc/hosts
を置くかのどちらかになる。
Lambda Function実行時に /etc/hosts
を作る
例えば、entrypointで touch /etc/hosts
を実行して空のhostsファイルを作ればよいのではないか?と思ったが、Read Only Filesystemのため書き込むことができない(/etc/hosts
のみ書き込み可能にできたりするんですかね?)。
コンテナイメージのビルド時に /etc/hosts
を仕込む
起動時がだめならビルド時だ、ということでDockerfile内でなんとか /etc/hosts
を配置できないか試行錯誤する。
RUNステップでtouchすればいいんじゃないかと思ったが、やはりRead Only Filesystemでだめ。
RUN touch /etc/hosts
最終的にどうしたかというと、COPYを使った。そもそも、AWSが提供しているイメージでには /etc/hosts
が含まれているため同様の問題は発生しない。じゃあどうやって /etc/hosts
を追加しているかというと、ADDを使ってtar.xzファイルを追加している( https://github.com/aws/aws-lambda-base-images/blob/ruby2.7/Dockerfile.ruby2.7 )。
なので、空のhostsファイルを用意して、Dockerfile内で /etc/hosts
として追加すればイメージに /etc/hosts
を含むことができる。
COPY docker/hosts /etc/hosts
ちなみに、COPYとADDどちらでも動くが、COPYを使うほうが望ましい。aws-lambda-base-imagesではtar.xzファイルを自動展開するためにADDを使っているが、今回のケースでは展開が不要である。なので、より挙動が明確なCOPYを使うとよい( ADD と COPY )。
これで、Buildkitによって空の /etc/hosts
ファイルがイメージに残っていた時と同じ状態にでき、暫定対処のコードがなくても動作するようになった。
そういうわけで、AWS Lambdaで自前のイメージを動かす場合はコードによっては /etc/hosts
ファイルを読もうとしてエラーになることがある、という話でした。