Node.jsのNative ModuleをAmazon Linux on Dockerでコンパイルする

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を利用するのは良い選択肢だと思いました。