JavaScript
browserify
serverless

Serverlessフレームワークの沼の話

More than 1 year has passed since last update.

この記事の内容

Serverlessフレームワークでgcloud(Google Cloud Platform API)を使おうとしたらがっつり沼にハマったのでそのメモ。さっさと別の方法で解決したほうが良かった気もするけど、誰かの役に立てばと...

前提

AWS Lambdaのデプロイ管理ができるフレームワーク「Serverless」のv0.5系を使っている。Lambdaには通常、単一のjsファイルか、(node_modulesを含んだ)zipファイルをアップロードする。そのzipパッケージが50MBまでという制限上、Serverlessではbrowserifyしたものをアップロードしてサーバーサイド(Lambda)で動かすという若干特殊なことをしている。

ここからが沼の話。やりたかったのはLambdaからGoogle BigQueryを操作すること。そのため、gcloudモジュールを使ってAPIを叩こうとした。すると下記のエラー。

Serverless: Error: Cannot find module './aes'
    at Function.Module._resolveFilename (module.js:339:15)
    at Function.Module._load (module.js:290:25)
    at Module.require (module.js:367:17)
    at require (internal/module.js:16:19)

まず疑ったのはbrowserifyの設定。今回の構成では、serverless-optimizer-pluginというプラグインでデプロイ時にbrowserifyが走るようになっている。こいつの該当コードを見る。

serverless-optimizer-plugin/index.js
      let b = browserify({
        basedir:          bundleBaseDir,
        entries:          [bundleEntryPt],
        standalone:       'lambda',
        extensions:       _this.config.extensions,
        browserField:     false,  // Setup for node app (copy logic of --node in bin/args.js)
        builtins:         false,
        commondir:        false,
        ignoreMissing:    true,  // Do not fail on missing optional dependencies
        detectGlobals:    true,  // Default for bare in cli is true, but we don't care if its slower
        insertGlobalVars: {      // Handle process https://github.com/substack/node-browserify/issues/1277
          //__filename: insertGlobals.lets.__filename,
          //__dirname: insertGlobals.lets.__dirname,
          process: function() {
          }
        }
      });

使ったこと無いオプションが並んでいたが、どうやらサーバーサイドでbrowserifyされたモジュールを使うオプションとしては正しそう。下記Issueが参考になった。

https://github.com/substack/node-browserify/issues/1540

serverless-optimizer-pluginでは、デプロイ時のみbrowserifyが実行され、ローカルでテストするときは通常通りCommonJSの依存関係解決が行われる。このままだとデバッグもし辛いので、同じ設定でbrowserifyを実行してみる。

$ browserify --ignore-missing --node -t babelify func/_handler.js -o func/handler.js

これで同じエラーが出る状態までもっていけた。

エラーが出ている場所はforgeモジュール内部の次の箇所。動的に実行されるrequire()はbrowserifyでは事前解決できない。よって依存モジュールがバンドルされず、実行時エラーになる。

forge/js/forge.js
var defineFunc = function(require, module) {
  module.exports = function(forge) {
    var mods = deps.map(function(dep) {
      return require(dep);
    });

次のIssueがまさにこの問題について。

https://github.com/digitalbazaar/forge/issues/215

どうやら解決する予定はあるらしい。でも予定は未定。詰んだ。

https://github.com/digitalbazaar/forge/issues/126

と思ってたら、forkされていい感じにしてくれているリポジトリを発見。ラッキー。

https://github.com/ysangkok/forge

これで前述のエラーが解消された!まあ次のエラーが出るんですけど。次は下記のモジュールでエラー。エラー内容メモするの忘れた...

https://github.com/stephenplusplus/hash-stream-validation

もう辛くなってきたけど、grepなり何なりでこいつに依存しているモジュールを突き止める。どうやらgcloudの中でもstorage APIだけらしい。今回使いたかったのはBig Query APIなので、どうにかこいつだけimport出来ないか試してみた。

通常.js
const gcloud = require('gcloud');
const bigquery = gcloud.bigquery({
  projectId: 'xxxxxxxxxxxx',
  credentials: {
    //...
  }
});

gcloudのリポジトリを見てみると、綺麗にディレクトリが分かれているので部分インポートが出来そうだった。しかし、やってみるとconfig_がないと怒られる。無理やり感があるが、https://github.com/GoogleCloudPlatform/gcloud-node/blob/master/lib/index.jsのコードを参考に、インポート文を次のように修正。

修正後.js
const gcloud = {
  config_: {},
  bigquery: require('gcloud/lib/bigquery')
};

const bigquery = gcloud.bigquery({
  projectId: 'xxxxxxxxxxxx',
  credentials: {
    //...
  }
});

まだエラーでる。。。このモジュール。

Error: Failed to find available CRC-32C implementation.

どうやらこのモジュールでまた動的なrequire()を実行しているらしい。
https://github.com/ashi009/node-fast-crc32c

下記が問題のコード。

node-fast-crc32c/loader.js
var fs = require('fs');

module.exports = (function(loaders) {

var impls = [
  './impls/sse4_crc32c_hw',
  './impls/sse4_crc32c_sw',
  './impls/js_crc32c'
];

for (var i = 0; i < impls.length; i++) {
  try {
    var crc32 = require(impls[i]);
    if (crc32.calculate("The quick brown fox jumps over the lazy dog") == 0x22620404)
      return crc32;
  } catch(e) {
  }
}

throw new Error('Failed to find available CRC-32C implementation.');

})();

どうしようもないのでforkして修正。./impls/sse4_crc32c_hwを読み込んでしまうと、ネイティブモジュールを実行するのでどのみちbrowserfiyでアウト。./impls/js_crc32cのみを事前ロードして実行するように修正。

https://github.com/KeitaMoromizato/node-fast-crc32c

修正後.js
var fs = require('fs');

module.exports = (function(loaders) {

var impls = [
  require('./impls/js_crc32c')
];

for (var i = 0; i < impls.length; i++) {
  try {
    var crc32 = impls[i];
    if (crc32.calculate("The quick brown fox jumps over the lazy dog") == 0x22620404)
      return crc32;
  } catch(e) {
  }
}

throw new Error('Failed to find available CRC-32C implementation.');

})();

ここまでで終わり。何とかデプロイして動くことが確認できた。

まとめ

そもそも「バックエンドで生まれたモジュールをフロントで使うためのbrowserifyをバックエンドで使う」というのがイケてないのではと...最初は便利かもと思ったけど。この辺りがうまくまとまらないのであれば(zipデプロイできる方法がある)、Serverlessに一元化は難しいなと思った。

あと、今回npm v3を使ったんだけど、一部のケースではflatten installされない事が分かった。今回のように、既存でインストールされているモジュールと同名のモジュール(forkしたもの)をインストールした場合、別の2つのモジュールとして認識される。そのためflattenな構造が崩れるので、デバッグ時には注意が必要。