DockerでAWS Lambda用のNode.jsネイティブモジュールをビルドする

DockerでAWS Lambda用のNode.jsネイティブモジュールをビルドする

先日、AWS LambdaのNode.jsでiconvを使うのが大変だった件と言う記事を書きましたが、その後すぐ、AWSがAmazon Linux用の公式Dockerイメージを公開している事を知りました。
そこで、前回はEC2上のAmazon LinuxでNode.jsのネイティブモジュールをビルドしましたが、今回はDockerを使った方法にリファクタリングしてみたいと思います。

尚、使用した環境はmacOS High Sierra (10.13.5) + Docker for Mac (18.05.0-ce-mac66, channel: edge)です。

Amazon Linuxイメージ

Docker Hubに公開されています。→ https://hub.docker.com/r/library/amazonlinux/
タグの一覧を見てみると、かなり細かくバージョンが切られているようです。

尚、AWS Lambdaでは現在の所Amazon Linux (amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2) が使われているので、それに併せて今回はamazonlinux:2017.03.1.20170812を使っていきます。まずはイメージをPullしてきます:

$ docker pull amazonlinux:2017.03.1.20170812

ネイティブモジュールをビルドする為のイメージを作成する

準備が整ったので早速作っていきます。まずは、先程のイメージを使ってコンテナを作成します:

$ docker run -it --name native-node amazonlinux:2017.03.1.20170812
bash-4.2#

※デフォルトで /bin/bash が起動するよう設定されています。

次にNode.jsをインストールします。今回はAWS Lambdaでv8.10のランタイムを使いたいので、それに併せて8系の物をインストールします。
公式ドキュメントのRHEL系でのインストール手順に従い、まずrpmを取得します (rootなのでsudoは省略します) :

bash-4.2# curl -sL https://rpm.nodesource.com/setup_8.x | bash -
...
## Run `sudo yum install -y nodejs` to install Node.js 8.x LTS Carbon and npm.
## You may also need development tools to build native addons:
sudo yum install gcc-c++ make
## To install the Yarn package manager, run:
curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
sudo yum install yarn

出力が上記のような形で終われば成功です。この後コメントの通り、yumを使ってNode.jsをインストールするのですが、 “You may also need development tools to build native addons: sudo yum install gcc-c++ make” にもある通り、まさしく今回はネイティブモジュールをビルドするので、これらも一緒にインストールする必要があります。従って、コマンドは次の通りになります:

bash-4.2# yum install -y nodejs gcc-c++ make
...

Installed:
gcc-c++.noarch 0:4.8.5-1.22.amzn1 nodejs.x86_64 2:8.11.2-1nodesource

Dependency Installed:
binutils.x86_64 0:2.25.1-31.base.66.amzn1 cpp48.x86_64 0:4.8.5-28.142.amzn1 gcc.noarch 0:4.8.5-1.22.amzn1
gcc48.x86_64 0:4.8.5-28.142.amzn1 gcc48-c++.x86_64 0:4.8.5-28.142.amzn1 glibc-devel.x86_64 0:2.17-222.173.amzn1
glibc-headers.x86_64 0:2.17-222.173.amzn1 kernel-headers.x86_64 0:4.14.42-52.37.amzn1 libgomp.x86_64 0:6.4.1-1.45.amzn1
libmpc.x86_64 0:1.0.1-3.3.amzn1 mpfr.x86_64 0:3.1.1-4.14.amzn1 python26.x86_64 0:2.6.9-2.89.amzn1
python26-libs.x86_64 0:2.6.9-2.89.amzn1

Dependency Updated:
glibc.x86_64 0:2.17-222.173.amzn1 glibc-common.x86_64 0:2.17-222.173.amzn1 libgcc48.x86_64 0:4.8.5-28.142.amzn1
libstdc++48.x86_64 0:4.8.5-28.142.amzn1 nss-softokn-freebl.x86_64 0:3.28.3-8.41.amzn1

Complete!

無事インストールされました。Node.jsは8.11がインストールされましたが、おそらく問題ないでしょう。

それでは、早速ネイティブモジュールのインストールをしてみたいと思います。前回と同様、iconvをインストールします。
テストなので、/tmpに移動してインストールします:

bash-4.2# cd /tmp
bash-4.2# npm install iconv

> iconv@2.3.0 install /tmp/node_modules/iconv
> node-gyp rebuild

make: Entering directory `/tmp/node_modules/iconv/build'
CXX(target) Release/obj.target/iconv/src/binding.o
CC(target) Release/obj.target/iconv/deps/libiconv/lib/iconv.o
CC(target) Release/obj.target/iconv/support/localcharset.o
SOLINK_MODULE(target) Release/obj.target/iconv.node
COPY Release/iconv.node
make: Leaving directory `/tmp/node_modules/iconv/build'
npm WARN saveError ENOENT: no such file or directory, open '/tmp/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/tmp/package.json'
npm WARN tmp No description
npm WARN tmp No repository field.
npm WARN tmp No README data
npm WARN tmp No license field.

+ iconv@2.3.0
added 2 packages in 6.425s

無事インストールができたみたいです。iconvの場合、ビルド成果物はnode_modules/iconv/build/Release/iconv.node以下に配置されるのですが、きちんとできています。

bash-4.2# ls -la node_modules/iconv/build/Release/iconv.node
-rwxr-xr-x 1 root root 1119920 Jun 5 05:53 node_modules/iconv/build/Release/iconv.node

問題は、これがLambdaで動くかどうかなのですが、前回、EC2上でビルドしたバイナリのmd5は 216d4e9181fc621c4500e1a6760f9e3f でした。
今回のバイナリはどうでしょうか? md5sum で検証してみましょう:

bash-4.2# md5sum node_modules/iconv/build/Release/iconv.node
216d4e9181fc621c4500e1a6760f9e3f node_modules/iconv/build/Release/iconv.node

(当然ですが)一致しました!大成功です。因みに、詳細は省きますが実際にLambda上にデプロイしても動作しました👍

コンテナの再利用

さて、無事ミッションに成功した所で、コンテナを今後も再利用していく為に、イメージ化していきたい所です。 docker commit を使って現在のコンテナの状態をイメージに保存ができますが、折角なのでDockerfile化しておきました → https://github.com/issei-m/npm-for-lambda

Docker Hubには公開していないので、手動でリポジトリをcloneして docker build  する必要があります:

$ docker build -t npm-for-lambda .

その後、npmをインストールしたいディレクトリを /root にボリュームマウントした状態で実行します。
尚、 npm がENTRYPOINTとして登録されているので、以降のコマンドのみを記述すればOKです:

$ docker run -it --rm -v /tmp:/root npm-for-lambda install iconv

> iconv@2.3.0 install /root/node_modules/iconv
> node-gyp rebuild

make: Entering directory `/root/node_modules/iconv/build'
CXX(target) Release/obj.target/iconv/src/binding.o
CC(target) Release/obj.target/iconv/deps/libiconv/lib/iconv.o
CC(target) Release/obj.target/iconv/support/localcharset.o
SOLINK_MODULE(target) Release/obj.target/iconv.node
COPY Release/iconv.node
make: Leaving directory `/root/node_modules/iconv/build'
npm WARN saveError ENOENT: no such file or directory, open '/root/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/root/package.json'
npm WARN root No description
npm WARN root No repository field.
npm WARN root No README data
npm WARN root No license field.

+ iconv@2.3.0
added 2 packages in 10.784s

無事作られました。チェックサムもばっちりです:

$ md5 /tmp/node_modules/iconv/build/Release/iconv.node
MD5 (/tmp/node_modules/iconv/build/Release/iconv.node) = 216d4e9181fc621c4500e1a6760f9e3f

おわりに

いかがでしたでしょうか。今回は、(今更ながら)Dockerがアプリケーションの実行環境だけでなく、特定の環境に向けてのコンパイル環境としても有用と言う事が分かりました。
今後も是非活用していきたいと思います。