💎

AWS LambdaでRuby(2.7)のnokogiriをサクッと動して、途中で発生したインストールエラーも読み解いていく

2022/10/22に公開

まえがき

RubyでnokogiriはWebスクレイピングする時によく使うと思いますが、AWS Lambdaだとちょっと手順がややこしかったりします。
これは、gem install nokogiriをしようとしてもnaitive extention周りでエラーが出ることに起因します(もう少し正確に言うとnokogiriが依存しているracc)

今回は、

  • どういうふうに設定するとAWS LambdaでサクッとNokogiriを動かせるか
  • どのようにエラーを追いかけたのか

を記載していこうと思います。

どういうふうに設定するとAWS LambdaでサクッとNokogiriを動かせるか

Lambda Layerもありますが、一番手っ取り早いのはコンテナイメージを使ってLambdaを動かすパターンだと思います。
今回は、AWS SAM(以下、sam)を使ってサクッと動かす手順を記載します。

samのインストール方法については以下を参照してみてください。
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html

sam(+Lambda image)を使う

$ sam init --name nokogiri-test --package-type Image --base-image amazon/ruby2.7-base --app-template hello-world-lambda-image

--app-templateオプションについては以下の記事が参考にさせていただきました。
https://kazuhira-r.hatenablog.com/entry/2022/01/08/005641

ファイルの編集をしていく

$ cd nokogiri-test/
$ cat <<EOF | patch hello_world/Gemfile
3a4
> gem "nokogiri"
EOF
$ cat <<EOF | patch hello_world/app.rb
2a3
> require 'nokogiri'
EOF
$ cat <<EOF | patch hello_world/Dockerfile
5a6
> RUN yum install -y gcc make
EOF

動かしてみる

$ sam build
$ sam local invoke -e events/event.json

どのようにエラーを追いかけたのか

今回、Lambda上でgccとmakeコマンドを追加でインストールする必要があったので、Dockerfileに追記しています。
このコマンドを必要と判断した流れを書いていきます。

単純にnokogiriを追記してsam buildしてみる

まず、単純にGemfileとapp.rbにnokogiriを追記して試してみます。

$ cd nokogiri-test/
$ cat <<EOF | patch hello_world/Gemfile
3a4
> gem "nokogiri"
EOF
$ cat <<EOF | patch hello_world/app.rb
2a3
> require 'nokogiri'
EOF
$ 
$ sam build
Your template contains a resource with logical ID "ServerlessRestApi", which is a reserved logical ID in AWS SAM. It could result in unexpected behaviors and is not recommended.
Building codeuri: /Users/st1t/nokogiri-test runtime: None metadata: {'DockerTag': 'ruby2.7-v1', 'DockerContext': '/Users/st1t/nokogiri-test/hello_world', 'Dockerfile': 'Dockerfile'} architecture: x86_64 functions: HelloWorldFunction
Building image for HelloWorldFunction function
Setting DockerBuildArgs: {} for HelloWorldFunction function
Step 1/5 : FROM public.ecr.aws/lambda/ruby:2.7
 ---> dfbb8874d088
Step 2/5 : COPY app.rb Gemfile ./
 ---> 6bcf60ef4340
Step 3/5 : ENV GEM_HOME=${LAMBDA_TASK_ROOT}
 ---> [Warning] The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
 ---> Running in 0e7081aca9ad
Removing intermediate container 0e7081aca9ad
 ---> 1af67b139d15
Step 4/5 : RUN bundle install
 ---> [Warning] The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
 ---> Running in 7ce8757ec8cb
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.
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Using bundler 2.3.22
Fetching mime-types-data 3.2022.0105
Fetching multi_xml 0.6.0
Fetching racc 1.6.0
Installing multi_xml 0.6.0
Installing racc 1.6.0 with native extensions
Installing mime-types-data 3.2022.0105
Fetching mime-types 3.4.1
Installing mime-types 3.4.1
Fetching httparty 0.20.0
Installing httparty 0.20.0
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /var/task/gems/racc-1.6.0/ext/racc/cparse
/var/lang/bin/ruby -I /var/lang/lib/ruby/site_ruby/2.7.0 extconf.rb
checking for rb_block_call()... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/var/lang/bin/$(RUBY_BASE_NAME)
/var/lang/lib/ruby/2.7.0/mkmf.rb:471:in `try_do': The compiler failed to
generate an executable file. (RuntimeError)
You have to install development tools first.
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:564:in `try_link0'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:582:in `try_link'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:794:in `try_func'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:1083:in `block in have_func'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:971:in `block in checking_for'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:361:in `block (2 levels) in postpone'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:331:in `open'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:361:in `block in postpone'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:331:in `open'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:357:in `postpone'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:970:in `checking_for'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:1082:in `have_func'
        from extconf.rb:6:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can
be found here:

  /var/task/extensions/x86_64-linux/2.7.0/racc-1.6.0/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /var/task/gems/racc-1.6.0 for inspection.
Results logged to
/var/task/extensions/x86_64-linux/2.7.0/racc-1.6.0/gem_make.out

  /var/lang/lib/ruby/site_ruby/2.7.0/rubygems/ext/builder.rb:102:in `run'
/var/lang/lib/ruby/site_ruby/2.7.0/rubygems/ext/ext_conf_builder.rb:28:in
`build'
/var/lang/lib/ruby/site_ruby/2.7.0/rubygems/ext/builder.rb:171:in
`build_extension'
/var/lang/lib/ruby/site_ruby/2.7.0/rubygems/ext/builder.rb:205:in `block in
build_extensions'
  /var/lang/lib/ruby/site_ruby/2.7.0/rubygems/ext/builder.rb:202:in `each'
/var/lang/lib/ruby/site_ruby/2.7.0/rubygems/ext/builder.rb:202:in
`build_extensions'
/var/lang/lib/ruby/site_ruby/2.7.0/rubygems/installer.rb:843:in
`build_extensions'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/rubygems_gem_installer.rb:72:in
`build_extensions'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/rubygems_gem_installer.rb:28:in
`install'
  /var/lang/lib/ruby/site_ruby/2.7.0/bundler/source/rubygems.rb:207:in `install'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/installer/gem_installer.rb:54:in
`install'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/installer/gem_installer.rb:16:in
`install_from_spec'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/installer/parallel_installer.rb:186:in
`do_install'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/installer/parallel_installer.rb:177:in
`block in worker_pool'
  /var/lang/lib/ruby/site_ruby/2.7.0/bundler/worker.rb:62:in `apply_func'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/worker.rb:57:in `block in
process_queue'
  /var/lang/lib/ruby/site_ruby/2.7.0/bundler/worker.rb:54:in `loop'
  /var/lang/lib/ruby/site_ruby/2.7.0/bundler/worker.rb:54:in `process_queue'
/var/lang/lib/ruby/site_ruby/2.7.0/bundler/worker.rb:91:in `block (2 levels)
in create_threads'

An error occurred while installing racc (1.6.0), and Bundler cannot continue.

In Gemfile:
  nokogiri was resolved to 1.13.9, which depends on
    racc

Build Failed
Error: HelloWorldFunction failed to build: The command '/bin/sh -c bundle install' returned a non-zero code: 5
$

以下の通り、nokogiriが依存しているraccのインストールでコケていることがわかりました。

An error occurred while installing racc (1.6.0), and Bundler cannot continue.

In Gemfile:
  nokogiri was resolved to 1.13.9, which depends on
    racc

次は、DockerfileのFROMで指定しているイメージを使って実際にもう少し詳しいエラー内容を追いかけていきます。

raccのインストールをFROMで指定しているコンテナで試す

Dockerfile内でFROMはFROM public.ecr.aws/lambda/ruby:2.7と指定しています。
そのイメージを使ってraccのインストールを試していきます。

$ docker run --rm -it -v $(pwd)/hello_world:/tmp --entrypoint bash public.ecr.aws/lambda/ruby:2.7
bash-4.2# cd /tmp
bash-4.2# gem install racc
Building native extensions. This could take a while...
ERROR:  Error installing racc:
        ERROR: Failed to build gem native extension.

    current directory: /var/lang/lib/ruby/gems/2.7.0/gems/racc-1.6.0/ext/racc/cparse
/var/lang/bin/ruby -I /var/lang/lib/ruby/site_ruby/2.7.0 extconf.rb
checking for rb_block_call()... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/var/lang/bin/$(RUBY_BASE_NAME)
/var/lang/lib/ruby/2.7.0/mkmf.rb:471:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:564:in `try_link0'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:582:in `try_link'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:794:in `try_func'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:1083:in `block in have_func'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:971:in `block in checking_for'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:361:in `block (2 levels) in postpone'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:331:in `open'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:361:in `block in postpone'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:331:in `open'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:357:in `postpone'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:970:in `checking_for'
        from /var/lang/lib/ruby/2.7.0/mkmf.rb:1082:in `have_func'
        from extconf.rb:6:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /var/lang/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/racc-1.6.0/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /var/lang/lib/ruby/gems/2.7.0/gems/racc-1.6.0 for inspection.
Results logged to /var/lang/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/racc-1.6.0/gem_make.out
bash-4.2# 

同じエラーの再現ができました。
エラーの内容をよく見ていると、

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /var/lang/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/racc-1.6.0/mkmf.log

と記載がありますので、確認してみます。

なお、エラーログにYou have to install development tools first.と記載があるので、yum groupinstall "Development Tools"で解決する可能性もありますが、大量に関係ないパッケージもインストールされ、脆弱性管理やsam buildしたときに時間が長時間になるので基本的には避けたほうが無難です(昔みたいに少数のサーバを継続して利用していくならともかく、コンテナの場合は都度groupinstallは長時間になるので時間がもったいないです)

bash-4.2# cat /var/lang/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/racc-1.6.0/mkmf.log
"gcc -o conftest -I/var/lang/include/ruby-2.7.0/x86_64-linux -I/var/lang/include/ruby-2.7.0/ruby/backward -I/var/lang/include/ruby-2.7.0 -I.    -g -O2 -fPIC conftest.c  -L. -L/var/lang/lib -Wl,-rpath,/var/lang/lib -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic     -Wl,-rpath,/var/lang/lib -L/var/lang/lib -lruby  -lm   -lc"
checked program was:
/* begin */
1: #include "ruby.h"
2: 
3: int main(int argc, char **argv)
4: {
5:   return !!argv[argc];
6: }
/* end */

bash-4.2# 

実行コマンドが記載されているみたいなので、そのまま実行してみます。

bash-4.2# gcc -o conftest -I/var/lang/include/ruby-2.7.0/x86_64-linux -I/var/lang/include/ruby-2.7.0/ruby/backward -I/var/lang/include/ruby-2.7.0 -I.    -g -O2 -fPIC conftest.c  -L. -L/var/lang/lib -Wl,-rpath,/var/lang/lib -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic     -Wl,-rpath,/var/lang/lib -L/var/lang/lib -lruby  -lm   -lc
bash: gcc: command not found
bash-4.2#

gccコマンド見つからないみたいでした。
gccをインストールしてもう一度試してみます。

bash-4.2# yum install -y gcc
bash-4.2# gem install racc
Building native extensions. This could take a while...
ERROR:  Error installing racc:
        ERROR: Failed to build gem native extension.

    current directory: /var/lang/lib/ruby/gems/2.7.0/gems/racc-1.6.0/ext/racc/cparse
/var/lang/bin/ruby -I /var/lang/lib/ruby/site_ruby/2.7.0 extconf.rb
checking for rb_block_call()... yes
checking for rb_ary_subseq()... yes
creating Makefile

current directory: /var/lang/lib/ruby/gems/2.7.0/gems/racc-1.6.0/ext/racc/cparse
make DESTDIR\= sitearchdir\=./.gem.20221022-192-1ge3yx2 sitelibdir\=./.gem.20221022-192-1ge3yx2 clean
current directory: /var/lang/lib/ruby/gems/2.7.0/gems/racc-1.6.0/ext/racc/cparse
make DESTDIR\= sitearchdir\=./.gem.20221022-192-1ge3yx2 sitelibdir\=./.gem.20221022-192-1ge3yx2
make failedNo such file or directory - make

Gem files will remain installed in /var/lang/lib/ruby/gems/2.7.0/gems/racc-1.6.0 for inspection.
Results logged to /var/lang/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/racc-1.6.0/gem_make.out
bash-4.2# 

まだ駄目ですね、エラーログを見ていきます。

よくエラーログを見たら以下のように出力されてました。
makeコマンドが見つからないと言われています。

make failedNo such file or directory - make

makeコマンドをインストールしてもう一度試してみます。

bash-4.2# yum install -y make
bash-4.2# gem install racc
Building native extensions. This could take a while...
Successfully installed racc-1.6.0
Parsing documentation for racc-1.6.0
Installing ri documentation for racc-1.6.0
Done installing documentation for racc after 2 seconds
1 gem installed
bash-4.2# 

ついに、raccのインストールに成功しました🎉🎉🎉
そのまま、コンテナ上でnokogiriのインストールを試してみます。

bash-4.2# gem install nokogiri
Fetching nokogiri-1.13.9-x86_64-linux.gem
Successfully installed nokogiri-1.13.9-x86_64-linux
Parsing documentation for nokogiri-1.13.9-x86_64-linux
Installing ri documentation for nokogiri-1.13.9-x86_64-linux
Done installing documentation for nokogiri after 9 seconds
1 gem installed
bash-4.2# 

nokogiriのインストールにも成功しました🎉🎉🎉
これで、エラーの解決はできたので、あとは冒頭にもある通り、Dockerfileにgccとmakeをインストールするコマンドを追記したら終わりです。

$ cat <<EOF | patch hello_world/Dockerfile
5a6
> RUN yum install -y gcc make
EOF
$
$ sam build && sam local invoke -e events/event.json
Your template contains a resource with logical ID "ServerlessRestApi", which is a reserved logical ID in AWS SAM. It could result in unexpected behaviors and is not recommended.
Building codeuri: /Users/st1t/nokogiri-test runtime: None metadata: {'DockerTag': 'ruby2.7-v1', 'DockerContext': '/Users/st1t/nokogiri-test/hello_world', 'Dockerfile': 'Dockerfile'} architecture: x86_64 functions: HelloWorldFunction
Building image for HelloWorldFunction function
Setting DockerBuildArgs: {} for HelloWorldFunction function
Step 1/6 : FROM public.ecr.aws/lambda/ruby:2.7
 ---> dfbb8874d088
~

Building image..................
Skip pulling image and use local one: helloworldfunction:rapid-1.60.0-x86_64.

START RequestId: 13d1c46b-adb9-41be-b57f-e55c05b41968 Version: $LATEST
END RequestId: 13d1c46b-adb9-41be-b57f-e55c05b41968
REPORT RequestId: 13d1c46b-adb9-41be-b57f-e55c05b41968  Init Duration: 1.17 ms  Duration: 963.76 ms     Billed Duration: 964 ms Memory Size: 128 MB     Max Memory Used: 128 MB 
{"statusCode":200,"body":"{\"message\":\"Hello World!\"}"}
$

app.rb内でrequire 'nokogiri'していてもエラーで落ちることなく、問題なく処理してくれました。

最後に

一見エラーの出力が多く、中身もイマイチどう追いかけたらよいかわからないパターンに見えますが、コマンドひとつひとつを切り分けて追いかけてみるとあっさりしていることがわかるかと思います。
(bundle installの出力だけで推測するのではなく、コンテナに入ってgem installしてみたり、出力のログを読み取ってみたり)

これは今回のnokogiriのインストール限らず、すべてのエラー調査に通ずるものがあります。
何か障害やエラーが起きたときは慌てず、英語であってもDeepL等の翻訳ソフトに頼りつつ、ひとつひとつ根気よく見ていけば大体の事象は見えてくるものがあるはずなので、落ち着いて見ていくと良さそうです。

Discussion