Lambda(Node.js)をES2015のPromiseを使って今っぽく書いてみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

モバイルアプリサービス部の五十嵐です。

私のNode.jsの知識は0.x系で止まっているので、今っぽい(と言ってもAWS Lambdaで使いたいのでv4.3.2の)書き方を調べてみました。その中でも一番良く使われるであろうPromiseの使い方をまとめてみます。

サンプルコードは、以前作成したLambda Function(bisque33/slack-slash-commands-with-serverless)をPromiseを使って書き直しました。実際の差分比較はこちらです。

環境

  • node.js 4.3.2
  • npm 2.14.12

Promiseとは

Promiseは、リンク先の言葉を借りると

Promiseオブジェクトは処理の延期(deferred)と非同期処理のために使われます。Promiseはまだ完了していないが、いずれ完了する処理を表します。

とあります。ScalaでいうFutureと似たような機能ですね。(ScalaのFutureについては【Scala】Future と未来のセカイが詳しいです。)

この機能を使うことで、Node.jsでありがちなcallback地獄を解消し、同期処理のように記述することができるようです。

AWS-SDKでPromiseを使う

JavascriptのAWS-SDKは、v2.3.0からPromiseに対応しています。

詳しい使い方は、以下の記事で紹介されています。

環境設定

実際に使ってみましょう。まずはじめに、実行環境にPromiseが定義されているかを確認します。

// Check if environment supports native promises
if (typeof Promise === 'undefined') {
  AWS.config.setPromisesDependency(require('bluebird'));
} else {
  console.log("Promiseは定義されています。");
}

以下は、このコードをAWS Lambda(v4.3.2)の実行環境で実行したログです。既にPromiseが定義されていることが分かります。

2016-09-11 12:07:29.294 (+09:00)           undefined          Promiseはは定義されています。
START RequestId: df8b2323-77cc-11e6-a30f-6db6cb958ee9 Version: $LATEST
2016-09-11 12:07:29.692 (+09:00)           df8b2323-77cc-11e6-a30f-6db6cb958ee9       {"errorMessage":"Command accepted."}
END RequestId: df8b2323-77cc-11e6-a30f-6db6cb958ee9
REPORT RequestId: df8b2323-77cc-11e6-a30f-6db6cb958ee9     Duration: 396.99 ms        Billed Duration: 400 ms            Memory Size: 1024 MB       Max Memory Used: 28 MB

Promiseが定義されていれば特別な準備をする必要はありません。環境にPromiseが定義されていない場合は AWS.config.setPromisesDependencybluebird などのPromiseを実装するライブラリを設定することで使えるようになります。

書き方

あるLambdaから別のLambdaを非同期で実行する部分をPromiseを使って書きなおしてみます。

before

const lambda = new AWS.Lambda();
lambda.invoke(options, (error, data) => {
  if (error) {
    console.error(error, error.stack);
    callback("Command failed.", null);
  } else {
    callback(null, "Command accepted.");
  }
});

after

const lambda = new AWS.Lambda();
const invokePromise = lambda.invoke(options).promise();
invokePromise.then((data) => {
  callback(null, "Command accepted.");
})
.catch((error) => {
  console.error(error, error.stack);
  callback("Command failed.", null);
});

ネストが1段浅くなりました。 lambda.invoke の後に .promise() を付け加えることでPromiseを返すオブジェクトを作成しています。後はこのオブジェクトに対して .then .catch() などのメソッドチェーンを繋いでいきます。これらはPromiseの処理が完了した後に同期的に処理されます。

自分で定義した関数でPromiseを使う

自分で定義した関数でもPromiseを使えるようにしてみます。やり方は簡単で、既存の関数からコールバック引数をなくし、処理部分を new Promise(function(resolve, reject) { ... }); で包んでやればよいです。

before

const asyncInvokeLambda = (params, callback) => {
  // 途中省略

  const lambda = new AWS.Lambda();
  const invokePromise = lambda.invoke(options).promise();
  invokePromise.then((data) => {
    callback(null, "Command accepted.");
  }).catch((error) => {
    console.error(error, error.stack);
    callback("Command failed.", null);
  });
};

after

const asyncInvokeLambda = (params) => {
  return new Promise((resolve, reject) => {
    // 途中省略

    const lambda = new AWS.Lambda();
    const invokePromise = lambda.invoke(options).promise();
    invokePromise.then((data) => {
      resolve("Command accepted.");
    }).catch((error) => {
      console.error(error, error.stack);
      reject("Command failed.");
    });
  });
};

こうすることで asyncInvokeLambda 関数は .then .catch() などのメソッドを使うことができるようになります。 Promise.resolve() が処理に成功した結果、 Promise. reject() が処理に失敗した結果を返します。

連続したPromise

何重にもなったcallbackから解放されるには .then でPromiseオブジェクトを返すことで、次の .then に結果を渡すことができます。実際に例を見てみましょう。

const kms = new AWS.KMS();
const decryptPromise = kms.decrypt(cipherText).promise();
decryptPromise    // 1つめのPromise
.then((data) => {    // 1つめのPromiseの結果
  token = data.Plaintext.toString('ascii');
  return asyncInvokeLambda(params);    // 2つめのPromise
})
.then((data) => {    // 2つめのPromiseの結果
  resolve(data);
})
.catch((error) => {
  reject(error);
});

このようにPromiseを連続して使うことで、ネスト一定の深さでおさまることが分かりました。

まとめ

ここまでPromiseの基本的な使い方を試してみました。Promiseを使うことで非同期処理を同期的に記述でき、callback地獄からも解放されることがわかりました。もっとスマートに書くならGenerator関数とかを使うと良いらしいので、試してみたいと思います。

参考