kakts-log

programming について調べたことを整理していきます

node.js nightlyバージョンで使えるようになった http2モジュールについて

概要

先日 node.jsにおいて http/2の実装がマージされました。
experimentalな機能で、現在だとnode nightlyバージョン(9.x系)で実行時にオプションを付けることで http2 コアモジュールが利用可能です。
近々node.js 8.xに取り込まれるようで年内にはちゃんと使えるようです。
今回は、このhttp2コアモジュールについて簡単な使い方と内部実装について調べたことをまとめます。

ドキュメントはnodeのmasterブランチにマージされており、見ることができます。
- github.com

http2コアモジュールの実装について

http2の実装はnghttp2というCライブラリが採用されております。 このライブラリはhttp/2の実装のなかで人気であり、 RFC7540 HTTP/2 とRFC7541の仕様に基づいて実装されています。
- github.com

http2 コアモジュールのJS apiは下記で定義されています。
- node/http2.js at master · nodejs/node · GitHub

http/2のJS apiの各実装は lib/internal/http2配下のファイルにあります。内部の実装を見たい人はここで見ることができます。
- node/lib/internal/http2 at master · nodejs/node · GitHub

nghttp2ライブラリへのインターフェースはsrc/node_http2.ccと src/node_http2.hで定義されています
nghttp2とコアモジュールのやり取りはこのあたりを見ればよいかと思います。
- node/node_http2_core.cc at master · nodejs/node · GitHub
- node/node_http2.h at master · nodejs/node · GitHub

上記のhttp2コアモジュールを使うにあたり、nodeの実行時に –expose-http2 フラグを追加することで、 require(‘http2’) が使えるようになります。
これは、すでに http2というnpmモジュールが存在しているため、 –expose-http2フラグを付けないと下記のnpmモジュールをrequireするようになり、既存のnpm http2モジュールを使っているプロジェクトは注意が必要です。
- www.npmjs.com

node.js nightly版のインストール

http2のコアモジュールを使うために、node.jsの最も最新のnightly版(現時点で9.x系)を使えるようにします。
mac OSの環境が前提ですが、 node.js nightly版を簡単にインストールできる node-nightlyというnpmモジュールを使います。

node-nightlyモジュールインストール
$npm install --global node-nightly

nightly最新版のnodeインストール
$ node-nightly 
Downloading the nightly version, hang on...
node-nightly is available on your CLI!

nightlyバージョンの確認
$ node-nightly -v
 New nightly available. To upgrade: `node-nightly --upgrade` 
v9.0.0-nightly20170806e96ca62480

これでnode.js nightly版を簡単にインストールすることができました。

http2.createSecureServerを使ってhttp2サーバを作る

ここでさっそくhttp2のTLS/SSLを使ったセキュアなサーバを作ってみます。
http2.createSecureServer()というメソッドで、サーバのインスタンスを作成することができます。

http2.createSecureServer()について

http2.createSecureServer()の内部の実装は、主要なところだけ抜粋すると以下のようになっています。 - https://github.com/nodejs/node/blob/master/lib/internal/http2/core.js#L2427-L2434

function createSecureServer(options, handler) {
  if (typeof options === 'function') {
    handler = options;
    options = Object.create(null);
  }
  debug('creating http2secureserver');
  // Http2SecureServerのインスタンスを返す
  return new Http2SecureServer(options, handler);
}

・・・

// Http2SecureServerクラス
class Http2SecureServer extends TLSServer {
  // コンストラクタ
  constructor(options, requestListener) {
    options = initializeTLSOptions(options);
    super(options, connectionListener);
    this[kOptions] = options;
    this.timeout = kDefaultSocketTimeout;
    this.on('newListener', setupCompat);
    if (typeof requestListener === 'function')
      this.on('request', requestListener);
    this.on('tlsClientError', onErrorSecureServerSession);
    debug('http2secureserver created');
  }

  // setTImeoutメソッド
  setTimeout(msecs, callback) {
    this.timeout = msecs;
    if (callback !== undefined) {
      if (typeof callback !== 'function')
        throw new errors.TypeError('ERR_INVALID_CALLBACK');
      this.on('timeout', callback);
    }
    return this;
  }
}

このメソッドの戻り値として、Http2SecureServerクラスのインスタンスを返します。
Http2SecureServerクラスはTLSServerクラスを継承しており、接続においてSSL/TLSを使う事となります。

TLSServerクラスはtlsというコアモジュールで定義され、実装は下記になります。

tls_wrapでTLSServerクラスの実装が見れます。
[https://github.com/nodejs/node/blob/master/lib/
tls_wrap.js#L784-L893]

かなり階層が深くなりますが、TLS/SSLを使ったセキュアなサーバを作る場合、http2.createSecureServerの引数にオプションを渡すことができ、 接続において使うTLS/SSLの証明書や秘密鍵を設定することができます。 optionとして、オブジェクトのkey, certにそれぞれ指定してあげます。
node/_tls_wrap.js at master · nodejs/node · GitHub

TLS/SSL証明書 秘密鍵の作成

今回はここで証明書と秘密鍵を指定するために、ローカルで自前で準備していきます。
ここではあくまでローカル環境でのhttp2サーバを作ることが目的のため、自前でローカルpcでopensslコマンドを使って作成します。
- stackoverflow.com

mac OSでopensslを使って以下のコマンドを実行することでカレントディレクトリに下記2つのファイルが生成されるので、 サーバインスタンス生成時にこれを指定します。
- server.crt: SSLサーバ証明書 - server.key: SSL公開鍵暗号用の秘密鍵

サーバを立ててみる

さっそく上記の説明を踏まえてhttp2サーバを立ててみます。
ここではあくまでhttp2コアモジュールのサンプルコードをそのまま使用します。

http2サーバインスタンス生成時に 証明書・秘密鍵を指定し、 3000番ポートで待ち受けます。
https://localhost:3000をブラウザでアクセスしてhello worldを表示させる簡単なものを作ります。

サーバを立てた後、アクセスがくると サーバインスタンスに対して"stream"イベントが発火されるので、 ハンドラ関数を設定します。
ここではresponse statusと hello worldを記述した簡単なhtmlをstreamで返します。

// http2コアモジュールをrequire
const http2 = require('http2');

// createSecureServerでのオプションを指定する
const options = {
  key: fs.readFileSync('server.crt'), // 公開鍵暗号の秘密鍵
  cert: fs.readFileSync('server.key') // TLS/SSL サーバ証明書
};

// Create a plain-text HTTP/2 server
const server = http2.createSecureServer(options);

// アクセスが来た場合、streamイベントが発火されう
server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end('<h1>Hello World</h1>');
});

server.listen(3000);

chromeでアクセスする

ここで、サーバを立ち上げる事ができたので、chromeブラウザで https://localhost:3000へアクセスしてみます。
f:id:kakts:20170809005348p:plain
上述したように、今回はTLS/SSL証明書を自前で用意したため、chromeでwarning画面がでますが、無視して、「詳細設定」→「localhost にアクセスする(安全ではありません)」をクリックすることでアクセスが可能です。

f:id:kakts:20170809005555p:plain

まとめ

これでhttp/2のセキュアなサーバにアクセスできました。 今回は簡単な開設のため、http/2固有の機能を使ったサーバの作り方は解説せず、ここで終わります。
http/2固有の機能を使うことでよりパフォーマンスが向上するため、時間があるときに、http/2RFCをちゃんと読んで仕様を把握していきたいと思います。

参考

qiita.com

d.hatena.ne.jp

babel ES6からES5へのトランスパイル

概要

babel.jsをつかって ES6のシンタックスで書かれたjavascriptファイルをES5のシンタックスにトランスパイルする方法をまとめました。
実務で使ったことなかったので勉強がてらさらっとまとめます。

babel-cliのインストー

babelをコマンドラインで利用するに当たって、babelのコマンドラインクライアントである、npm モジュールのbabel-cliをインストールする必要があります。

www.npmjs.com

開発環境のみで使うので –save-devでpackage.jsonに対してdevDependenciesにモジュールの情報を追記させます。

$ npm install --save-dev babel-cli

今回はサンプルとして今回 hapiというhttpサーバモジュールを使うのでこちらもインストールしておきます

$ npm install --save hapi

.babelrcにプラグイン設定記述

babelでトランスパイルする際に、利用するプラグインを指定する必要があります。
今回はES6(ES2015とも言う)からES5へトランスパイルするため、ES2015 presetを指定します。
babeljs.io

babelコマンドの –presetsオプションでも指定できますが、毎回打つのは面倒なので、 babelの設定を記述するファイルである .babelrcを作成し、json形式で記述します。

.babelrc

{
  "presets": ["es2015"]
}

presets項目に使用するプラグイン名を指定します。

トランスパイル元のjsファイル作成

ここで、トランスパイル元のjsファイルである src/index.jsを作成します。
hapiモジュールを使って簡単なhttpサーバを作ります。
全てES6(ES2015)のシンタックスで記述します。

import Hapi from 'hapi';

const server = new Hapi.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// Add routing
server.route({
  method: 'GET',
  path: '/hello',
  handler: function(request, reply) {
    console.log('[/hello] requested');
    reply('hello world');
  }
});

server.start();

これを importや constを使わない、ES5形式にトランスパイルさせます。

babelコマンドをつかったトランスパイル

トランスパイルした後のファイルは dist/index.jsに作成させるとします。

さっそくコマンドラインでbabelコマンドを使ってトランスパイルさせます。
babelコマンドの –out-fileオプションをつかって、トランスパイル後のファイルの出力先を指定します。

$ babel src/index.js --out-file dist/index.js

これによって、 dist/index.jsにES5形式にトランスパイルされたjsファイルができあがります。

'use strict';

var _hapi = require('hapi');

var _hapi2 = _interopRequireDefault(_hapi);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var server = new _hapi2.default.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// Add routing
server.route({
  method: 'GET',
  path: '/hello',
  handler: function handler(request, reply) {
    console.log('[/hello] requested');
    reply('hello world');
  }
});

server.start();

元のjsファイルで使っていた import, const が使われずにそれぞれ require varを使うようにトランスパイルされているのが確認できます。

babelをつかったトランスパイルの手順は以上となります。 まだ試していませんが、最新のES2017形式のファイルをES5にトランスパイルする場合は ES2017 presetを使えばできます。
babeljs.io

node.js stringからbufferへの変換

node.jsにおいて、文字列をbufferへ変換させたいときの方法をまとめます

環境
node.js 8.1.3

Bufferインスタンスをどう生成するか

node.js 8.x系におけるBufferインスタンスの作成において、 new Bufferは既にdepricatedになっている*1ため、ここではbufferインスタンスの作成はBuffer.allocを利用します。

Buffer | Node.js v8.1.3 Documentation

Buffer.alloc と Buffer.allocUnsafe

Buffer.alloc(size)の他に、インスタンス生成時にデータを初期化させないBuffer.allocUnsafe(size)があります。
Buffer.allocは、インスタンス生成時にデータをzero-filledする初期化コストがかかるため、Buffer.allocUnsafeよりパフォーマンスは劣りますが、sensitiveなデータを含まないことが保証されるので、速度を最優先させる場面以外において、基本的にBuffer.allocを使う方が良いかと思います。
https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_class_method_buffer_allocunsafe_size

文字列をbufferに変換する場合において、Buffer.allocを利用時にBufferサイズ指定する必要があります。 下記に文字列をBufferに変換するスクリプトにあるように、ここで毎回文字列のサイズを渡してBufferインスタンスを生成します。

function bufferFromString(str) {
  // Allocates a new Buffer of size bytes.
  // If fill is undefined, the Buffer will be zero filled;
  // set size of string length
  var buffer = Buffer.alloc(str.length)
  buffer.write(str)
  return buffer
}

const text1 = bufferFromString('hello');
const text2 = bufferFromString('wor');

console.log('----text1', text1);
console.log('----text2', text2);

// text1のbufferにworを書き込む
text1.write('wor');

// 先頭の3文字分 のみ更新させるため、4,5文字目は変わらない
// hello → worlo
console.log(text1.toString('utf8'));

ただ、欠点なのは毎回Buffer.allock(size)でインスタンスを生成した後、Buffer.write()でデータを書き込む必要があるため、若干冗長です。

Buffer.from

Buffer.fromの引数に文字列を渡すと、一発で文字列をBufferに変換できます。

https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_class_method_buffer_from_string_encoding

> Buffer.from("hello world")
<Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>

まとめ

文字列からBufferへの変換方法をざっとまとめました。 Buffer.alloc Buffer.allocUnsafe Buffer.fromはそれぞれ挙動に違いがあるので、公式ドキュメントをちゃんと読んで、状況に応じて使い分けるとよいかと思います。

参考

Uncaught TypeError: Cannot read property 'split' of undefined · Issue #185 · Keyang/node-csvtojson · GitHub

AWS LambdaからDynamo DBにデータを追加する

概要

AWS lambda functionが呼び出された際に、AWS dynamoDBのテーブルにデータを追加していく方法についてまとめます。

今回は、API Gateway と lambdaで構築した line botに対して送信されたデータを dynamoDBに追加していくところまで説明します。

IAMユーザの設定

AWSの IAM manageページから、新規にIAMユーザを作成します。
このユーザは、Dynamo DBに対する書き込み、読み込み権限そして、lambda関数からdynamoを使う権限を付与します。

「ユーザを追加ボタン」でユーザ名を入力し、「アクセスの種類」で、プログラムによるアクセスと、AWSマネジメントコンソールへのアクセスを許可します。

ユーザの作成を完了すると、 「アクセスキーID」と、「シークレットアクセスキー」が取得できます。 この2つをlambdaからdynamoDBへのアクセス時に必要なので、メモしておきます。

作成後、IAMユーザの設定画面に移り、「アクセス権限を追加」ボタンをおし、アクセス権限を追加していきます。
予めグループを作成し、そのグループにアクセス権限を追加する方法もありますが、今回はユーザに対してアクセス権限を追加します。

「アクセス許可の付与」画面で、「既存のポリシーを直接アタッチ」項目を選択し、もともとAWS側で用意されているポリシーをユーザに追加します。 DynamoDB用のアクセス権限を追加するため、「AmazonDynamoDBFullAccess」ポリシーを探し、選択します。

f:id:kakts:20170625154758p:plain

これでIAMユーザを作成し、そのユーザに対してlambdaからDynamoDBへのデータの読み書きする権限を付与できました。

DynamoDBの設定

DynamoDBにデータを追加するためのテーブルを作成します。

DynamoDBのマネジメントコンソールにアクセスし、「テーブルの作成」ボタンからテーブル作成画面に遷移します テーブルの設定はデフォルトにし、テーブル名、プライマリーキーを設定します。
プライマリーキーはDynamoDBのテーブル内のアイテム毎にユニークなIDのことです。 本稿では、テーブル名はmessage プライマリーキーは_id と設定していきます

f:id:kakts:20170625162946p:plain

これでDynamoDBのテーブルを作成することができました。

lambda関数実装

IAMユーザの作成、DynamoDBのテーブルも作成できたので、次にlambda関数で、linebotに対して送られたメッセージをDynamoDBに保存する関数を実装していきます。
前回の 「aws lambdaとapi gatewayで linebotを作成する」で作成したlambda関数をベースに、DynamoDBにデータを追加する処理を実装します。

AWSの各サービスに対するクライアントsdkは公式で用意されており、

kakts-tec.hatenablog.com

npm の aws-sdkモジュールがあるので、コレを利用します。
www.npmjs.com

DynamoDBへのアクセス時に、先程IAMユーザの作成時に生成されたAPIアクセスキーとシークレットアクセスキーを利用します。
コードに直接埋め込むのは管理上よくないので、lambdaの環境変数に指定します。

  • DYNAMO_API_ACCESS_KEY : DynamoDBアクセス用IAMユーザのアクセスキー
  • DYNAMO_USER_SECRET_ACCESS_KEY : DynamoDBアクセス用IAMユーザのシークレットアクセスキー

DynamoDB用クライアントインスタンスの作成は下記の用な感じで行います。
exports.handlerの内部で記述すると、関数が読み込まれるたびに初期化が走るため、exports.handlerの外部で初期化させます。

const https = require('https');
const AWS = require('aws-sdk');

// dynamoDBクライアントインスタンス初期化
// IAMユーザのAPIアクセスキー、シークレットアクセスキーを指定する。  
// region apiVersionも指定する。  
const dynamo = new AWS.DynamoDB({
    region: 'ap-northeast-1',
    apiVersion: '2012-08-10',
    accessKeyId: process.env.DYNAMO_API_ACCESS_KEY,
    secretAccessKey: process.env.DYNAMO_USER_SECRET_ACCESS_KEY
});

登録するデータは、linebotから来るリクエストデータにはいっている、ユーザID、メッセージタイプ(text や sticker[スタンプ]など)、ユーザが入力したテキスト を登録していきます。

メッセージが送信される毎にデータを登録していくため、ユニークキーであるidには一意なものを指定します。
ここでは、とりあえず $USERID
$DATETIME を指定します。

データの登録は、DynamoDBインスタンスのputItemメソッドを使って行います。

http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#putItem-property

上記ドキュメントに書かれているサンプルコードですが、パラメータにテーブル名、そして 追加するアイテムを指定してきます。
アイテム内の各アトリビュートは、文字列(S)や数値(N) など、それぞれ登録するデータの型をキーに指定します。 このあたりの仕様はドキュメントを参照してください。

/* This example adds a new item to the Music table. */

 var params = {
  Item: {
   "AlbumTitle": {
     S: "Somewhat Famous"
    }, 
   "Artist": {
     S: "No One You Know"
    }, 
   "SongTitle": {
     S: "Call Me Today"
    }
  }, 
  ReturnConsumedCapacity: "TOTAL", 
  TableName: "Music"
 };
 dynamodb.putItem(params, function(err, data) {
   if (err) console.log(err, err.stack); // an error occurred
   else     console.log(data);           // successful response
   /*
   data = {
    ConsumedCapacity: {
     CapacityUnits: 1, 
     TableName: "Music"
    }
   }
   */
 });

実際に書いたlambda関数は以下になります。

const https = require('https');
const AWS = require('aws-sdk');

// dynamoDBクライアントインスタンス初期化
// IAMユーザのAPIアクセスキー、シークレットアクセスキーを指定する。  
// region apiVersionも指定する。  
const dynamo = new AWS.DynamoDB({
    region: 'ap-northeast-1',
    apiVersion: '2012-08-10',
    accessKeyId: process.env.DYNAMO_API_ACCESS_KEY,
    secretAccessKey: process.env.DYNAMO_USER_SECRET_ACCESS_KEY
});

const tableName = 'message';

// linebotから送信されたメッセージデータから返信用メッセージデータを作成する
// 本稿では、来たメッセージをそのまま返す
function createResponseMessage(messageData) {
  const message = messageData.message;
  let resMessage;
  if (message.type === 'text') {
    // text
    resMessage = {
      type: message.type,
      text: message.text
    }
  } else if (message.type === 'sticker') {
    // stamp
    resMessage = {
      type: message.type,
      packageId: message.packageId,
      stickerId: message.stickerId
    };
  }

  return resMessage;
}

exports.handler = (event, context, callback) => {
    const messageData = event.events && event.events[0];
    const replyToken = messageData.replyToken;
    const message = messageData.message;
    const text = message.text;
    const source = messageData.source;
    const resMessage = createResponseMessage(messageData);
    const data = JSON.stringify({
       replyToken: replyToken,
       messages: [resMessage]
    });

    // LINE reply apiのレスポンス設定
    const Authorization = 'Bearer ' + process.env.ENTER_ACCESS_TOKEN;
    opts = {
        hostname: 'api.line.me',
        path: '/v2/bot/message/reply',
        headers: {
            "Content-type": "application/json; charset=UTF-8",
            "Authorization": Authorization
        },
        method: 'POST',
    };

    const req = https.request(opts, function(res) {
        res.on('data', function(res) {
            console.log(res.toString());
        }).on('error', function(e) {
            console.log('ERROR: ' + e.stack);
        });
    });
    req.write(data);
    req.end();

    // DynamoDBへのメッセージデータ追加
    const userId = source.userId;
    const params = {
        TableName: tableName,
        Item: {
            _id: {
                S: userId + '_' + Date.now(),
            },
            userId: {
                S: userId
            },
            messageType: {
                S: message.type
            }
        }
    };
    // スタンプでなく文字列が投稿されたとき、textアトリビュートを追加する
    if (message.type === 'text') {
        params.Item['text'] = {
            S: message.text
        };
    }

    dynamo.putItem(params, function(err, data) {
        if (err) {
            console.error("Error occured", err);
        }
        console.error(data);
    });
};

最後に、linebotに対してメッセージを送信したあと、DynamoDBコンソールでデータが追加されていることを確認します。 f:id:kakts:20170625165955p:plain

aws lambdaとapi gatewayで linebotを作成する

概要

AWSAPI Gateway、lambdaを利用し、ユーザからのメッセージが来たときにlambda関数を呼び出し、ユーザへレスポンスを返すline botを作ります。

行った作業手順を一通りまとめます。

Linebotアカウントの作成

まず、line messaging api用のbotアカウントの作成を行うため、line business centerへログインします。
business.line.me 「アカウントリスト」タブを選択し、「アカウント作成」→「Messaging APIを始める」で、アカウントを作成する事ができます。

作成後、line@MANAGERページにて作成したアカウントの設定を行っていきます。

アカウント設定開始するとBot設定画面に遷移します。 Bot設定項目の中で「Webhook送信」項目を「利用する」に設定することで、messaging apiを利用できます。
f:id:kakts:20170622224241p:plain

またline business centerへもどり、「アカウントリスト」→作成したアカウントの「LINE Developers」から、botアカウント用のapiアクセストークンを確認することができます。

f:id:kakts:20170622224604p:plain 「ISSUE」ボタンを押すことでアクセストークンを確認できます。 このアクセストークンは後ほど使います。

API Gateway で新しいAPIの作成

awsコンソールから「サービス」→「Amazon API Gateway」を選択し、新しいAPIの作成します。
「新しい API」にチェックを入れ、適当なAPI名を入力します。
f:id:kakts:20170622234300p:plain

いったんおいておき、lambda関数の作成を行います。

aws lambdaの記述

早速aws lambdaを使ってユーザからのメッセージを受け取り、messaging apiを使ってレスポンスするための関数を書いていきます。

「Lambda関数の作成」ボタンから 「設計図の選択」画面に遷移し、そこで、lambdaで使うランタイムと、関数のサンプルを選ぶことができます。
ランタイムはNode.js と pythonから選ぶことができ、本稿では Node.js 6.10を選択します。
関数のサンプルは、「Blank Function」を選択します。 f:id:kakts:20170622231032p:plain

続いて、「トリガーの設定」画面に遷移し、lambdaを実行するためのトリガーを選択します。
本稿では、lambda関数へのエンドポイントとして、 AWS API Gatewayを利用しますので、「API Gateway」を選択します。
作成時に下記の3つの入力項目を設定します。 - API名 任意の名前 - デプロイされるステージ prod - セキュリティ AWS IAM

line botに対してユーザから送られるメッセージは、下記のreply APIの仕様に基づいて送信されます。 ユーザから受け取ったリクエストから、replyToken, textを取得し、reply api を利用することでユーザに対して返信することができます。
LINE API Reference

ここでは、ユーザから来たメッセージをシンプルにそのまま返すようにします。

lambda関数は、awsのlambda関数の設定画面で入力できるため、今回はそのまま入力していきます

const https = require('https');

exports.handler = (event, context, callback) => {
    // メッセージデータ取得
    const messageData = event.events && event.events[0];

    
    const replyToken = messageData.replyToken;
    const message = messageData.message;
    const text = message.text;
    
    const data = JSON.stringify({
       replyToken: replyToken,
       messages: [{type: "text", text: text}]
    });
    
    // line messaggin apiのアクセストークンを環境変数ACCESS_TOKENに設定する 
    const Authorization = 'Bearer ' + process.env.ACCESS_TOKEN;
    opts = {
        hostname: 'api.line.me',
        path: '/v2/bot/message/reply',
        headers: {
            "Content-type": "application/json; charset=UTF-8",
            "Authorization": Authorization
        },
        method: 'POST',
    };

    const req = https.request(opts, function(res) {
        res.on('data', function(res) {
            console.log(res.toString());
        }).on('error', function(e) {
            console.log('ERROR: ' + e.stack);
        });
    });
    req.write(data);
    req.end();
};

lambda関数が毎回実行されるたびに、デフォルトでexports.handler メソッドが呼ばれます。 引数は3つあり、 event.events内に、lineユーザからのメッセージのリクエストデータが格納されます。

line reply APIのリクエスト時に、先程「LINE Developers」管理画面で取得した アクセストークンが必要で、 リクエストヘッダーの Authorization に 追加します。

コードの中に直接アクセストークンを埋め込むと、管理上面倒なため、lambdaの環境変数を利用します。
f:id:kakts:20170622232955p:plain

上記のように、キーと値を入力することで、lambda関数内部で process.env.ACCESS_TOKENとして呼び出して使うことができます。

他にもいろいろ設定する項目があり、代表的なものをあげますが それ以外の項目はデフォルトのままでよいかと思います。

関数の設定
名前:  任意

Lambda 関数ハンドラおよびロール
・ハンドラ   index.handler
・ ロール     「テンプレートから新しいロールを選択」
・ロール名   任意のロール名

詳細設定
メモリ:128MB

api gatewayとlambda関数を紐付ける

ここで、作成した API GatewayによるAPIと lambda関数を紐付けます。
lambdaの設定画面より「トリガー」タブを選択→「トリガーを追加」を選択 → 「API Gatway」を選択し、先程作成したAPIAPI名、デプロイされるステージ、セキュリティ項目を入力します。

f:id:kakts:20170622234715p:plain

「メソッドの作成」→「POST」を選択しチェックボタンを押し、下記のように作成したlambda関数の設定を入れます。
f:id:kakts:20170622235021p:plain

これで、API Gatewayによって作った apiに対して POSTでリクエストを送ると lambda関数を呼び出すといって一連の設定ができました。

line managerで作成したapiの登録を行う

最後に、 LINE Developer管理画面で、Webhook URLを設定していきます。
先程のlambdaの設定画面の「トリガー」タブに、連携したAPIのURLが表示されています。

これを LINE Developer管理画面 に登録します。
登録後、Webhook URL 項目に「verify」ボタンができるので、これを押すとapiとの疎通確認を行います。
これでsuccessが出たら設定完了です。

verify時に下記の文言がでてエラーが出た場合は、再度api gateway と lambdaの設定を見直す必要があります

A http status of the response was '502 Bad Gateway'.

verify時にlambdaのcloud watchログを見ると reply apiで下記のエラーが出ていますが、api gatewayと lambdaの設定自体が正しければ successになっているので、特に問題は無いかと思います。

{"message":"Invalid reply token"}

linebotにメッセージを送る

これで一通りの設定が完了したので、実際にbotに対してメッセージを送ってみます。
成功すると以下の通りにメッセージを返してくれるようになります。 f:id:kakts:20170623001606j:plain

うまくいかない場合は lambdaのcloudwatchのログを見て、lambda関数内でエラーが出ているか確認することと、API Gatewayの設定が正しいか再度確認することが大事です。

確率的データ構造・ブルームフィルタについてのまとめ

概要

特定のデータが、ある集合やリストに含まれるかどうかを判定するために線形探索や二分探索などいくつかのサーチアルゴリズムが使われますが、
本稿ではメモリの使用効率、探索の際の計算量が優れているブルームフィルタを用いたアルゴリズムについてまとめます。

ブルームフィルタとは

ブルームフィルタとは確率的データ構造の一種で、あるデータが集合に属するかどうかを判定する際に使われます。
特徴としては、メモリの空間消費が少なくすみ、非常に効率的にデータの存在判定ができます。
デメリットとしては、false-positive(偽陽性)によるデータの誤判定の可能性があり、集合に存在しないデータを存在すると誤判定してしまう場合があります。このような誤判定が許されないような状況においては、ブルームフィルタは使えません。 逆に、false-negative(偽陰性)による誤判定はないため、データは実際にあるのに存在しないと判定することはありません。

ブルームフィルタ - Wikipedia

ブルームフィルタのデータ構造について

ハッシュベースサーチは集合Cのすべての要素をリンクトリストもしくはオープンアドレスハッシュテーブルHに格納する どちらのケースにおいても、多くの要素がハッシュテーブルに格納され、要素の格納に係る時間は、要素数が増えるに従い増えていく。 実際、この振る舞いは他のサーチアルゴリズムにも共通したもので、要素の探索にかかる比較時間とのトレードオフとなる。

ブルームフィルターはビット配列Bを提供し、集合CからBに追加することと、特定の要素EがBに存在するかどうかの判定は定数時間で行うことができ、驚くことに、これは既にBに格納されたデータ数とは独立しています。
しかし、ブルームフィルターにおいては、要素がBに存在するか判定する際に、 false-positiveである場合があります。 つまり、実際には存在しないのに、存在すると判定してしまう場合があります(日本語で言うと偽陽性

False-positiveになる簡単な例

例 下記のテーブルは、u,v,w,zの4つのデータに対して、ハッシュ関数3つそれぞれで得られたハッシュ値をまとめています。

ハッシュ値1 ハッシュ値2 ハッシュ値3
u 1 3 8
v 2 4 8
w 1 2 6
z 1 2 3

u, v, wの3つのデータを集合Cに追加したときのビット配列は 下記の通りになるとします。

配列のインデックス 0 1 2 3 4 5 6 7 8 9
ビット 0 1 1 1 1 0 0 0 1 0

このとき、このビット配列をもとに、u, v, w, zのそれぞれが集合Cに存在するかを判定します。

各データに対して計算された 3つのハッシュ値をビット配列のインデックスとみなし、ビット配列の各インデックスの値をチェックします。
データuについて見てみると、インデックスは 1, 3, 8となります。 ビット配列の 1, 3, 8番目の値をみると、それぞれ1となるので、データuが集合Cに存在すると判定されます。 これは、実際にデータuは集合Cに追加されているので、正しい振るまいです。データv, wにおいても同様に存在すると判定されます。

ここで、実際に集合Cに追加されていないデータzについて見てみます。 3つのインデックスは 1, 2, 3となりますが、それぞれビット配列の要素を調べてみると、どれも1となり、データzは集合Cに存在していないに、存在すると判定されます。 これがfalse-positiveの一例です。
言うまでもなく、ブルームフィルタの実装において、このfalse-positiveの割合が低ければ低いほど、ブルームフィルタの精度が高くなります。
この精度向上のためには、ハッシュ値を均等に出力するハッシュ関数をどう実装するか、さらにはビット配列の長さをどうするかも重要になります。

pythonですが簡単な実装は以下の通りになります。

Best, Average Worst: O(k)

create(m)
    return bit array of m bits
end

add (bits, value)
    # Kビットある場合、ビット毎にの桁を計算するためK個分のハッシュ関数がある
    foreach hashFunction hf
        ## << left シフトオペレータをつかって2^(hf)を効率的に計算できる
        setbit = 1 << hf(value)
        bits |= setbit
end

search(bits, value)
    foreach hashFunction hf
        checkbit = 1 << hf(value)
        # もしチェックビットが0だったら求める値は存在しない
        if checkbit | bits = 0 then
            return false

         # false-positiveすべてのビットがセットされているが求める値は追加されていない
    return true
end

ブルームフィルタの入出力

ブルームフィルタはデータをハッシュベースサーチのように処理する。 アルゴリズムはmビットのすべてが0になっているビット配列Mからスタートする。 データが追加される時、k個のハッシュ関数が、Mの配列内のビットの位置を計算する。

ブルームフィルタのコンテキスト

ブルームフィルタは、メモリの使用量が少なくすみ、非常に効率的ですが、利用可能な条件があり、 それは先程説明したfalse-positiveに関して許容されるときです。

有効的な活用法としては、探索コストの高いデータ探索(diskドライブへのアクセスが行われる場合)などに、前段として ブルームフィルタを導入し、false-positiveなケースはあるが、事前にデータ存在判定を行う事が挙げられます。

solution

ブルームフィルタは mビットのデータが必要です。 下記の実装は

class bloomFilter:
    def __init__(self, size = 1000, hashFunctions=None):
        """
        指定したサイズのビット数をもつブルームフィルタを作成する。
        同時にハッシュ関数も作成する
        """

        self.bits = 0
        self.size = size

        if hashFunctions is None:
            self.k = 1
            self.hashFunctions = [lambda e, size : hash (e) % size]
        else:
            self.k = len(hashFunctions)
            self.hashFunctions = hashFunctions

    def add(self, value):
        """
        ブルームフィルタにデータを追加する
        """
        for hf in self.hashFunctions:
            self.bits |= 1 << hf(value, self.size)

    def __contains__(self, value):

        """
        valueがブルームフィルタに存在するか判定する。
        ブルームフィルタの性質上 false-positiveな状態が発生しうる
        この場合、実際にデータは存在しないのにtrueを返してしまう。
        しかし false-negativeは起こり得ない
        """
        for hf in self.hashFunctions:
            if self.bits & 1 << hf(value, self.size) == 0:
                return False

            return True

この実装は、k個のハッシュ関数が存在していることを前提としており、各ハッシュ関数

データが追加されるときは毎回、k個のビットが、ハッシュ関数によって計算されたビット位置に対してセットされます。

分析

ブルームフィルタに必要なメモリ容量は mビットで固定されている。
このメモリ容量は、追加されるデータ量に応じて増加することはありません。

さらに、このアルゴリズムはデータの探索と追加毎にk回操作しか行わないため、O(k)時間で1回の操作が完了します。 kは数値なので、定数時間で処理ができるためかなり速いです。

各データに対して、偏りなく効率的にビット値を計算するアルゴリズムを設計するのは非常に難しいことで、ビット配列のサイズは定数だが、false-positiveが発生するレートを極力下げる必要があります。

さらに重要な特徴としては、ブルームフィルタはデータを削除することは仕組み上できないです。
例をあげると、もし1つのデータを消す場合 k個のハッシュ関数によって(1, 3, 5 ,8, 9)というハッシュ値を持っていた場合、他のデータで(1, 3, 5 ,8, 9)のハッシュ値のうちどれか1つでも持っているデータに関する情報もブルームフィルタから消えてしまうため、判定できなくなるためです。

まとめ

ブルームフィルタのデータ構造、探索ロジックについて簡単ですがざっくりとまとめました。
ブルームフィルタを利用する上で考慮すべき、false-positiveの発生確率については、また別記事でまとめたいと思います。