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の機能を期待します)。

ぜひおためしください!