カテゴリー内の他の記事

AWS Lambda 連携 -PDF変換ツールを作ってみた-

フォローする

はじめに

今回はAWS Lambdaの便利な機能Scheduled Functionsを使って、kintone連携を試してみたいと思います。
kintone 単体ではできなかった、サーバー側の機能拡張をつくっていきたいと思います。

具体的にはAWS Lambdaを用いて、kintoneにアップロードされた複数枚の画像を一つのPDFに変換するツールを作っていきたいと思います。

尚、検証にはkintone環境を事前にご準備いただく必要があります。 cybozu developer networkでは、アカウントを取得いただければ無償でkintone「開発者ライセンス」をご取得できますので、まだアカウント取得していない方はご登録ください。

 

kintone x AWS Lambda連携

kintoneアプリの作成

次のようなアプリを作成します

フィールドタイプ 標題 フィールドコード 追加設定
文字列一行 タイトル title -
添付ファイル 画像 images -
添付ファイル PDF pdf -
チェックボックス チェックボックス processed 項目:processed

AWS アカウントのセットアップ

こちらのページを参考にして、AWSアカウントのセットアップと管理者ユーザーの作成を行ってください。利用を開始してから1年間は、無料利用枠の範囲で利用することができます。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/setting-up.html

Lambdaの利用を開始する

サービスメニューからLambdaを開きます。何も設定していない場合は以下のような画面が表示されます。「Get Started Now」と書かれたボタンをクリックしてください。

あらかじめ用意されたテンプレートを利用することができます。言語設定のドロップダウンからNode.jsを選択して、hello-world というタイトルのテンプレートをクリックします。

今回、トリガーはkintoneからのリクエストにするのでこの画面は「Next」を押します。

ファンクションの設定

以下のように設定します。

■Configure function
 Name: pdfConverterForKintone
 Description: A starter AWS Lambda function
 Runtime: NodeJS

■Lambda function code
   Code entry type:
Edit code inline

■Lambda function handler and role
   Handler:
index.handler 
   Role:
Basic execution role
   Exsting role :
任意のロール

■Advanced settings
   Memory(MB): 128
   Timeout: 0min 30 sec
   VPC: No VPC

設定を完了後「Next」を押すと確認画面に遷移します。
確認画面にて「Create function」をクリックするとファンクションが作成されます。

この状態で左上の「Test」をクリックすると、ファンクションが実行され、動作を確認することができます。

では、早速kintoneと連携するプログラムを作っていきましょう。

kintoneからレコードを取得する

コードエディタを編集して、以下のコードを入力します。

var https = require('https');
var querystring = require('querystring');

const kintoneHost = '.cybozu.com';
const appId = ;
const apiToken = '';

var getOptions = function(path, method) {
    return {
        hostname: kintoneHost,
        port: 443,
        path: path,
        method: method,
        secureProtocol: 'SSLv3_method',
        headers: {
            'X-Cybozu-API-Token': apiToken
        }
    };
};

var getRecords = function(callback) {
    console.log('start getRecords');
    var params = {
        "app": appId,
        "query": 'processed not in ("processed")'
    };
    var query = querystring.stringify(params);
    var options = getOptions('/k/v1/records.json?' + query, 'GET');
    var req = https.request(options, function(res) {
        console.log('STATUS: ' + res.statusCode);
        console.log('HEADERS: ' + JSON.stringify(res.headers));
        res.setEncoding('utf8');
        res.on('data', function(chunk) {
            console.log('BODY: ' + chunk);
            if (res.statusCode === 200) {
                callback(null, JSON.parse(chunk).records);
            }
        });

    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        callback(e.message);
    });

    req.end();
};

exports.handler = function(event, context) {
    var operation = event.operation;

    getRecords(function(err, records) {
        if (err) {
            context.fail(err);
        } else {
            context.succeed(records.length);
        }
    });
    return;

};

kintoneHost, appId, apiTokenは適宜ご利用の環境に合わせて差し替えてください。編集が終わったら、左上の「Save and test」をクリックします。処理が正常に実行されると、緑色のチェックマークとともに、「Execution result: succeeded」のメッセージが表示されます。
このプログラムでは、処理済みのフラグが立っていないレコードの件数を戻り値として返しています。プログラムで出力されたログを見れば、きちんとkintoneからデータが返ってきていることがわかります。

kintoneからファイルをダウンロードする

次に、kintoneからファイルをダウンロードする処理を書いてみます。

var getFile = function(file, callback) {
    console.log('start getFile');
    var options = getOptions('/k/v1/file.json?fileKey=' + file.fileKey, 'GET');

    var fileName = '/tmp/' + file.fileKey + path.extname(file.name);
    postProcessResource(fileName);

    var req = https.request(options, function(res) {
        console.log('STATUS: ' + res.statusCode);
        console.log('HEADERS: ' + JSON.stringify(res.headers));
        res.on('data', function(chunk) {
            if (res.statusCode === 200) {
                console.log(chunk.length + ' chunked');
                fs.appendFileSync(fileName, chunk, 'binary');
            }
        });
        res.on('end', function() {
            if (res.statusCode === 200) {
                callback(null, fileName);
            }
        });
    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        callback(e.message);
    });

    req.end();
};

// ファイルを削除する
var postProcessResource = function(resource, fn) {
    var ret = null;
    if (resource) {
        if (fn) {
            ret = fn(resource);
        }
        try {
            fs.unlinkSync(resource);
        } catch (err) {
            // Ignore
        }
    }
    return ret;
};

Lambdaでは、/tmpディレクトリ以下に500MBまで、一時的なファイルを置くことができます。この関数ではファイルキーと拡張子をファイル名として、/tmpディレクトリに一時ファイルを生成しています。添付ファイルのサイズが大きい場合、一度のdataイベントで全てのデータが返らないことがあるため、fs.appendFileSyncでデータをファイルに追加していくようにしています。全てのデータがダウンロードし終わったら、callbackを呼び出してファイル名を呼び出し元に渡します。

画像ファイルをPDFに変換する

LambdaのNode.jsでは、ImageMagickを利用することができます。ImageMagickはパワフルな画像処理ライブラリで、Webサービスによく用いられます。ImageMagickを使えば、複数の画像ファイルから簡単にPDFファイルを生成することができます。

var convert = function(inputFiles, callback) {
    var args = [];
    console.log('start converting');
    for (var i = 0; i < inputFiles.length; i++) {
        if (args.length > 0) {
            args.push('-adjoin');
        }
        args.push(inputFiles[i]);
    }
    var outputFile = '/tmp/' + (new Date()).getTime() + '-images.pdf';
    args.push(outputFile);
    console.log(args);

    im.convert(args, function(err, output) {
        if (err) {
            console.log('Convert operation failed:', err);
            callback(err);
        } else {
            console.log('Convert operation completed successfully');
            inputFiles.forEach(function(inputFile) {
                postProcessResource(inputFile);
            });
            callback(null, outputFile);
        }
    });
};

kintoneにファイルをアップロードする

Lambdaからkintoneにファイルをアップロードするには、multipart/form-data形式でファイルをアップロードする必要があります。この形式のデータ生成を行ってくれるライブラリもあるようなのですが、今回は自力でやってみました。boundaryは適当な長さの文字列であればなんでも構いません。

var postFile = function(outputFile, callback) {
    console.log('start postFile');

    var options = getOptions('/k/v1/file.json', 'POST');
    var boundary = 'afdasfd77a6s234ak3hs7';
    options.headers['Content-Type'] = 'multipart/form-data; boundary="' + boundary + '"';

    var req = https.request(options, function(res) {
        console.log('STATUS: ' + res.statusCode);
        console.log('HEADERS: ' + JSON.stringify(res.headers));
        res.on('data', function(chunk) {
            console.log('BODY: ' + chunk);
            if (res.statusCode === 200) {
                callback(null, JSON.parse(chunk));
            }
        });
    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        callback(e.message);
    });

    req.write(
        '--' + boundary + '\r\n' + 'Content-Type: application/octet-stream\r\n' + 'Con-tent-Disposition: form-data; name="file"; filename="images.pdf"\r\n' + 'Content-Transfer-Encoding: binary\r\n\r\n'
    );
    stream = fs.createReadStream(outputFile, {
        bufferSize: 4 * 1024
    });
    stream.on('data', function(chunk) {
        req.write(chunk);
    });
    stream.on('end', function() {
        req.end('\r\n--' + boundary + '--');
    });
};

 

kintoneのレコードを更新する

最後に、アップロードしたファイルキーでkintoneのレコードを更新します。さらに処理済みのフラグをオンにします。

var putRecord = function(id, fileKey, callback) {
    console.log('start putRecord');
    var params = {
        "app": appId,
        "id": id,
        "record": {
            pdf: {
                value: [{
                    fileKey: fileKey
                }]
            },
            processed: {
                value: ['processed']
            }
        }
    };
    var json = JSON.stringify(params);
    var options = getOptions('/k/v1/record.json', 'PUT');
    options.headers['Content-Type'] = 'application/json';

    var req = https.request(options, function(res) {
        console.log('STATUS: ' + res.statusCode);
        console.log('HEADERS: ' + JSON.stringify(res.headers));
        res.setEncoding('utf8');
        res.on('data', function(chunk) {
            console.log('BODY: ' + chunk);
            if (res.statusCode === 200) {
                callback(null, JSON.parse(chunk));
            }
        });

    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
        callback(e.message);
    });

    req.write(json);
    req.end();
};

非同期処理を行う

JavaScriptではうまく非同期処理を扱わないと非常にコードが汚くなってしまいます。最近のNode.jsではPromiseがサポートされているのですが、Lambdaで利用されているバージョンではサポートされていないようなので、今回はasyncライブラリを使用することにしました。ライブラリを使う場合、以下のようにライブラリごとzipに固めて Lambdaにアップロードする必要があります。

npm install async
zip -r kintone-lambda.zip index.js node_modules

ここまでのプログラムをまとめたものをこちらで公開しています。
https://gist.github.com/ryokdy/cad4af0bb8cbc80c9661

実行してみよう

プログラムを実行すると、kintone に登録した画像ファイルがPDFに変換され、アップロードされていますね!
注意)
PDFの変換には一時的に多くのメモリを消費するため、画像のサイズなどによっては変換が失敗することがあります。必要に応じてメモリの設定を行ってください(メモリ量によって無料利用枠で実行できる時間が異なりますので注意してください)。

できたPDFは以下のように各画像をまとめた結果となります

定期実行を行う

CloudWatchにて定期実行の設定をおこなっていきます。「Create rule」にてルールを作成していきます。
ファンクションを実行する周期を設定できます。cronのように実行時刻を細かく設定することもできます。
この設定を行うことで、5分ごとにkintoneに新しいレコードがないかを確認し、あればPDFに変換するという処理を行えるようになります。

 

名前と説明をいれます。

これで5分間隔で定期実行ができます。

終わりに

いかがでしたか? 今回はLambda内で完結する処理を行いましたが、AWSの他のサービスを組み合わせることでさらに可能性が広がります。 例えば、こんなこともできそうです。

  • kintoneにアップロードした画像の文字認識や顔認識を行う
  • アップロードした動画をElastic transcoderエンコードしてストリーミング配信する
  • 定期的にデータをS3にバックアップする

Lambdaが定期実行をサポートしたことで、kintoneに対して定期的に処理を行うことが容易になりました。kintone側に手を入れなくて良いというメリットもあります(欲を言えばkintone側にもWebフックの機能を期待します)。

ぜひおためしください!

最後にここまでお読みいただき、ありがとうございます。
冒頭でも紹介しましたが、本サイトdeveloper networkでは、1年間無償のkintone developerライセンスをはじめとして以下の特典を用意しています。

★developer network 登録者特典★

  • 5ユーザー、1年間無償のkintone開発者ライセンス(申込アカウントごとに1つの環境)を申し込むことができます。
  • コミュニティに投稿やコメントしたり、Tips に質問できます。
  • API のアップデート情報の通知を受け取ることができます。
  • Tipsやサンプルの動きを確認できるデモサイトを利用することができます。
  • HTTP Client Tool for kintoneを利用することができます。

 まだ登録されてない方は以下よりご登録ください!

 

 

記事に関するフィードバック

直接的に記事と関連がないご質問はcybozu developer コミュニティをご活用ください。

ログインしてコメントを残してください。
Powered by Zendesk