Node.js
S3
lambda

lambdaのNode.jsでS3にあるオブジェクトをどうにかしようとしたら詰まったのを解決した

初めてNode.jsを書いて存分に詰まったので後学のためにまとめました。

やりたかったこと

S3に定期実行で保存されていく複数のログファイルを、一定の期間ごとに集計して集計結果をS3に保存したい

使っている技術、サービス

  • Node.js
  • lambda
    • cloud watchとかいうので定期実行できると聞いて選定
    • 社内でNode.jsマンが少ないからやってみよう的な安易な気持ち
  • S3

最初のつまづき

Node.js
var aws = require('aws-sdk');
var s3 = new aws.S3();

var bucket = 'bucket';
var dir = 'dir';

exports.handler = (event, context, callback) => {

    var params = {
        Bucket: bucket,
        Delimiter: '/',
        Prefix: dir + '/' + date + '/'
    }

    s3.listObjectsV2(params, function(err, data) {

        // 集計したいデータを複数のログファイルから取ってきて一つにまとめる
        var data = [];
        for (var i = 0; i < data.length; i++) {
            s3.getObject({Bucket: bucket, Key: 'key.json', (err, data) => {
                if(err) console.log(err);
                data.push(JSON.parse(data.Body.toString()));
            });
        };

        // 一つにまとめたデータを集計する
        var result = syuukeiFunction();

        // 結果をjson形式でs3に保存
        var body = JSON.stringify(result);
        var params = {
            Bucket: bucket,
            Key: 'key.json',
            Body: body,
            ContentType: 'application/json',
            ACL: 'public-read-write'
        };
        s3.putObject(params, function(err, data) {
           if (err) console.log(err, err.stack);
           context.done();
        });


    });
};

結果 => bodyが空でうまく保存できず。

ごく単純なミスで、Node.jsは非同期処理だから、ログファイルを一個一個取ってきて〜まとめて〜なんて重たい処理をする前にs3.putObjectが実行されているだけでした。

Promiseで非同期処理

Node.js
var aws = require('aws-sdk');
var s3 = new aws.S3();

var bucket = 'bucket';
var dir = 'dir';

exports.handler = (event, context, callback) => {
    var result = [];

    // Promiseを作る関数
    const makePromise = () => {
        return new Promise(function (resolve, reject) {
            s3.getObject({Bucket: bucket, Key: 'key.json', (err, data) => {
               if(err) console.log(err);
               var data = JSON.parse(data.Body.toString());
               Array.prototype.push.apply(result, data);

               resolve(result)
            });
        });
    }

    var params = {
        Bucket: bucket,
        Delimiter: '/',
        Prefix: dir + '/' + date + '/'
    }

    s3.listObjectsV2(params, function(err, data) {

        var Promises = [];
        // 必要なログデータの数だけPromiseを作る
        for (var i = 0; i < data.length; i++) {
            Promises.push(makePromise());
        };

        // Promiseが全部終わってから集計処理
        Promise.all(Promises)
          .then((resolve) => {
              //集計処理をやってresultとして吐き出す
              return result;
          })
          .then(_ => {
              var body = JSON.stringify(this);
              var params = {
                  Bucket: bucket,
                  Key: 'key.json',
                  Body: body,
                  ContentType: 'application/json',
                  ACL: 'public-read-write'
              };
              s3.putObject(params, function(err, data) {
                  if (err) console.log(err, err.stack);
                  context.done();
              });
          })
          .catch(function(error){
            console.log('error', error)
          });
    });
};

結果=>resultが空の状態で次の処理が始まって失敗

Promiseの中でobjectのデータがresultに追加される前にresolveが実行されてしまう。。。

この失敗をする前に、New PromiseをreturnしてなかったからPromisesがundefinedオブジェクトの配列になってしまったり、resolveをし忘れて永遠に処理が終わったことにならなかったりしましたが割愛。

最終形態

Node.js
var aws = require('aws-sdk');
var s3 = new aws.S3();

var bucket = 'bucket';
var dir = 'dir';

exports.handler = (event, context, callback) => {

    const makePromise = () => {
        return new Promise(function (resolve, reject) {
            var promise = new Promise(function(resolve,reject) {
                s3.getObject({Bucket: bucket, Key: 'key.json', (err, data) => {
                  if(err) console.log(err);
                  resolve(JSON.parse(data.Body.toString()));
                });
            })

            trends.then((data) => {
              Array.prototype.push.apply(array, data);
              resolve();
            }).catch((err) => console.error(err));

        });
    }

    var params = {
        Bucket: bucket,
        Delimiter: '/',
        Prefix: dir + '/' + date + '/'
    }

    s3.listObjectsV2(params, function(err, data) {

        var Promises = [];
        for (var i = 0; i < data.length; i++) {
            Promises.push(makePromise());
        };

        Promise.all(Promises)
          .then((resolve) => {
              //集計処理をやってresultとして吐き出す
              return result;
          })
          .then(_ => {
              var body = JSON.stringify(this);
              var params = {
                  Bucket: bucket,
                  Key: 'key.json',
                  Body: body,
                  ContentType: 'application/json',
                  ACL: 'public-read-write'
              };
              s3.putObject(params, function(err, data) {
                  if (err) console.log(err, err.stack);
                  context.done();
              });
          })
          .catch(function(error){
            console.log('error', error)
          });
    });
};

これでやっと成功。

Promiseの中にさらにPromiseを書いて、thenで親Promiseのresolveをするとかいう入り組んだ状態になったところで動いたので諦めました。

LambdaでNode.js v7が使えるようになったらawaitも使えるようになるらしいのでそれまでこのままかも。