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

著者名:門屋 亮(クローバ株式会社)

目次

はじめに

こんにちは。
クローバの門屋です。

AWS Lambda の Scheduled Functions がリリースされました。
Scheduled Functions とは、AWS のサーバー側でプログラムを定期実行するしくみのことです。
このしくみを kintone と組み合わせれば、kintone 単体ではできなかった、サーバー側の機能拡張を行うことができます。

今回は AWS Lambda を用いて、kintone にアップロードされた複数枚の画像をひとつの PDF に変換するツールを作ってみます。

kintone x AWS Lambda連携

kintoneアプリの作成

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

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

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

Lambda の開始方法 (External link) を参考にして、AWS アカウントのセットアップと管理者ユーザーの作成をしてください。
利用を開始してから 1 年間は、無料利用枠の範囲で利用できます。

Lambdaの利用を開始する

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

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

ファンクションの設定

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

  • Name: pdfConverterForkintone
  • Description: A starter AWS Lambda function
  • Runtime: Node.js 6.10
  • Handler: index.handler
  • Role: Basic execution role(新しくロールを作成します)
  • Advanced settings: Timeout を 30 sec にする。

「Create function」をクリックするとファンクションが作成されます。

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

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

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

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

サンプルコードでは、通信暗号方式に TLS v1.0 を利用していますが(16 行目)、現在 cybozu.com では無効化されています。
Dealing-with-Protocol-Methods (External link) を参考に、TLS v1.2 以降の値を指定してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const https = require('https');
const querystring = require('querystring');

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

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

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

  });

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

  req.end();
};

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

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


};

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const getFile = function(file, callback) {
  console.log('start getFile');
  const options = getOptions('/k/v1/file.json?fileKey=' + file.fileKey, 'GET');

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

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

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

  req.end();
};

// ファイルを削除する
const postProcessResource = function(resource, fn) {
  let 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 ファイルを生成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const convert = function(inputFiles, callback) {
  const args = [];
  console.log('start converting');
  for (let i = 0; i < inputFiles.length; i++) {
    if (args.length > 0) {
      args.push('-adjoin');
    }
    args.push(inputFiles[i]);
  }
  const outputFile = '/tmp/' + (new Date()).getTime() + '-images.pdf';
  args.push(outputFile);
  console.log(args);

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const postFile = function(outputFile, callback) {
  console.log('start postFile');

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

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

  req.on('error', (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', (chunk) => {
    req.write(chunk);
  });
  stream.on('end', () => {
    req.end('\r\n--' + boundary + '--');
  });
};

kintoneのレコードを更新する

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const putRecord = function(id, fileKey, callback) {
  console.log('start putRecord');
  const params = {
    app: appId,
    id: id,
    record: {
      pdf: {
        value: [{
          fileKey: fileKey
        }]
      },
      processed: {
        value: ['processed']
      }
    }
  };
  const json = JSON.stringify(params);
  const options = getOptions('/k/v1/record.json', 'PUT');
  options.headers['Content-Type'] = 'application/json';

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

  });

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

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

非同期処理を行う

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

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

ここまでのプログラムをまとめたものを kintone-lambda (External link) で公開しています。

実行してみよう

プログラムを実行すると、kintone で登録した画像ファイルが PDF に変換され、アップロードされていますね!

warning
注意

PDF の変換は一時的に多くのメモリを消費します。
そのため、画像のサイズなどによっては変換が失敗することもあります。
必要に応じてメモリを設定してください(メモリ量によって無料利用枠で実行できる時間が異なりますので注意してください)。

定期実行を行う

さて、プログラムができたところで、いよいよ定期実行を設定します。
「Triggers」タブを表示させたのち、「Add trigger」をクリックします。
「trigger type」に「CloudWatch Events - Schedule」を選択します。

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

終わりに

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

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

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

ぜひおためしください!