Node.jsのNative ModuleをAmazon Linux on Dockerでコンパイルする
AWS LambdaでNode.jsのNative Moduleを使いたい場合、ModuleをLinux環境でコンパイルする必要があります。
Amazon Linux on EC2を起動しその上でコンパイルする、あるいはVirtual Box等でRedhat系OS(CentOSなど)を起動してコンパイルすることも可能かもしれません。Vagrantなどを使えばいい感じにこれらを自動化できるかと思いますが、コンパイルのためにわざわざ仮想マシンを起動するのは若干面倒です(EC2の場合はAWSの利用料も発生します)。
今回は別の方法として、Amazon Linux on Dockerを利用してNative Moduleをコンパイルする方法をご紹介します。
開発環境
- OS : macOS Sierra 10.12.3
- Docker Community Edition for Mac : 17.03.0
Dockerについてはこちらからインストールを行なってください。
Amazon LinuxのDockerイメージ
2016年の11月に公開され、Amazon EC2 Container Registry (Amazon ECR)およびDocker Hubの公式リポジトリから入手可能となっています。 また今現在リリースされているイメージのバージョンは最も古いもので「2016.09.0.20161028」となっています。
一方でLambdaの実行環境は「2016.03.3」ですのでAmazon LinuxのDockerイメージとはバージョンが異なりますが、Node.jsのNative Moduleのコンパイル目的であればこのバージョン差異は特に問題にはならないかと思います。
Native Moduleコンパイル用のイメージ作成
今回は題材として画像ファイル変換用のNative Moduleであるsharpをコンパイルするためのイメージを作成します(※このパッケージを題材に選んだ特別な理由はありません。たまたまsharpを使う要件があったため、です)。
Amazon LinuxのDockerイメージを元に、sharpのコンパイルに必要な以下のパッケージをインストールしたDockerイメージを作成します。
- Node.js
- gcc-c++
コンパイルに必要なパッケージについてはAddon毎に異なりますので、それぞれのAddonのREADMEなどを参照ください。
またDockerイメージの作成方法としてDockerfileを使う場合とPackerを使う場合の2つの方法をご紹介します。
Dockerfileを使う場合
以下のような内容でDockerfileを作成します。
FROM amazonlinux:latest MAINTAINER Yutaka Yawata RUN ["/bin/bash", "-c", "curl -s https://rpm.nodesource.com/setup_4.x | bash -"] RUN ["/bin/bash", "-c", "yum install -y gcc-c++ nodejs"] CMD ["npm", "install", "--only=production"]
CMD
でコンテナ起動時に「npm install」を実行するように設定します。またnpmの「package.json」の「dependencies」にあるパッケージのみをインストール対象とするよう「--only=production」オプションを指定します。
CMD
の設定は必須ではなく、docker run
実行時に指定する形でももちろん構いません。
Dockerイメージを作成します。
$ docker build -t yuyawata/amazonlinux_nodejs .
Packerを使う場合
以下のような内容で「packer.json」を作成します。
{ "builders": [{ "type": "docker", "image": "amazonlinux:latest", "commit": "true", "changes": [ "CMD [\"npm install --only=production\"]" ] }], "provisioners": [{ "type": "shell", "inline": [ "curl -s https://rpm.nodesource.com/setup_4.x | bash -", "yum install -y gcc-c++ nodejs" ] }], "post-processors": [{ "type": "docker-tag", "repository": "yuyawata/amazonlinux_nodejs", "tag": "latest" }] }
Dockerイメージを作成します。
$ packer build packer.json
Native Moduleのコンパイル
まずはシンプルにLambdaファンクション本体(index.js)とpackage.jsonのみの構成の場合の例です。index.jsやpackage.jsonと同じ階層にNative Moduleをインストールします。
├── index.js └── package.json
{ "name": "native_addon_example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "sharp": "^0.17.2" } }
以下のようにdocker run
コマンドを実行します。
$ docker run --rm -v "$(pwd)":/opt -w /opt yuyawata/amazonlinux_nodejs:latest
docker run
コマンドのオプションは以下の通りです。
--rm
でコンテナ実行完了後にコンテナを自動削除-v
でホスト(MacOS)のカレントディレクトリとコンテナの「/opt」ディレクトリを共有(※opt以外のディレクトリでも構いません)-w
でコンテナの作業ディレクトリを「/opt」に設定 → Dockerfile(またはpacker.json)のCMD
で指定したnpm install
コマンドが「/opt」で実行される
docker run
コマンドを実行するとindex.jsやpackage.jsonと同じ階層に「node_modules」ディレクトリが作成され、その下にDockerコンテナ上でコンパイルされたNative Moduleがインストールされます。
Native Moduleのインストール先を指定する
実際のLambdaファンクションの開発では、
- 開発用のnpmパッケージを除外してバンドルを作成
- ES2015で開発&Babelでビルドしてバンドルを作成
といったワークフローが想定されます。
この場合は開発用のディレクトリ(src)とバンドル作成用のディレクトリ(dist)を分けて、バンドル作成時に必要なもののみを後者にコピーないしインストールする、というやり方が考えられます。
今回作成したDockerイメージを使って「dist」ディレクトリにNative Moduleをインストールするには、以下のように「package.json」を「dist」ディレクトリにコピーした上でdocker run
コマンドを実行します。
$ cp -f ./package.json ./dist $ docker run --rm -v "$(pwd)":/opt -w /opt/dist yuyawata/amazonlinux_nodejs:latest ## または $ docker run --rm -v "$(pwd)":/opt -w /opt yuyawata/amazonlinux_nodejs:latest npm install --only=production --prefix dist
docker run
の-w
オプションか、npm install
の--prefix
オプションで「/opt/dist」以下でnpm install
を実行するようにします。
--prefix
オプションを使用する場合は、Dockerfile(またはpacker.json)のCMD
で指定したnpm install
コマンドを上書きする形で、docker run
コマンドに--prefix
オプション付きでnpm install
コマンドを渡してやる必要があります。
まとめ
今回はNode.jsのNative Moduleを取り上げましたが、Node.jsに限らずクロスコンパイルツールとしてDockerを利用するのは良い選択肢だと思いました。