Edited at

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

More than 3 years have 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な構造が崩れるので、デバッグ時には注意が必要。