テキスト処理コマンドをServerLessにしてみる
Serverless Advent Calendar 2016、9日目の記事です。
最近Microsoft Azure Functionを触りはじめてみました。
そこでふと関数毎にデプロイできるのなら、UNIX系OSのシェルコマンドのように、処理をパイプ的な感じで接続したら面白そうかもと思い、どんな感じになるか試してみました。
動作イメージ
動作イメージとしては、以下のようなテキスト処理コマンドをServerLess(というかAzure Functions)上で処理してみる感じになります。
$ cat foo.txt
bbb
aaa
eee
ccc
bbb
ddd
fff
$ cat foo.txt | sort | uniq | head -n5 | nl
1 aaa
2 bbb
3 ccc
4 ddd
5 eee
Functions間でのテキストデータのやりとり
Functions間でのテキストは、JSON経由でやりとりします。Azure FunctionsではJSONのレスポンスが{"json":{...}}
という形で返されるので、それに合わせた内容でやりとりするのが良さそうです。
{"json":{"cmd":"cat","text":["hello"]}}
Functionsの例
headコマンドの動作を実現する例を示します。以下のような感じで、HTTP POSTメソッドで渡されてきたJSONデータのreq.body.json.cmd
でコマンドを分類することにします。
処理した結果はreq.body.json.text
に格納しておき、これをレスポンスのデータとして使用します。
module.exports = function(context, req) {
if (req.body.json && req.body.json.text) {
...
if (req.body.json.cmd === 'head') { // 先頭5行まで出力
req.body.json.text = req.body.json.text.slice(0, 5);
}
...
Function全体は以下となります。今回は単一のFunctionsの中でreq.body.json.cmd
により処理を分ける実装にしてみました。
module.exports = function(context, req) {
if (req.body.json && req.body.json.text) {
if (req.body.json.cmd === 'cat') { /* do nothing */ }
if (req.body.json.cmd === 'nl') { // 'cat -n'
var new_text = new Array();
for (var i in req.body.json.text) {
new_text.push((parseInt(i) + 1) + ' ' + req.body.json.text[i]);
}
req.body.json.text = new_text;
}
if (req.body.json.cmd === 'sort') {
req.body.json.text.sort(function (a, b) {
if (a > b) return 1;
if (a < b) return -1;
return 0;
});
}
if (req.body.json.cmd === 'uniq') {
var uniqed_text = req.body.json.text.filter(function (x, i, self) {
return self.indexOf(x) === i;
});
req.body.json.text = uniqed_text;
}
if (req.body.json.cmd === 'head') { // 先頭5行まで出力
req.body.json.text = req.body.json.text.slice(0, 5);
}
if (req.body.json.cmd === 'wc') {
req.body.json.text = [ req.body.json.text.length ];
}
context.res = {
json: req.body.json
};
}
else {
context.res = {
status: 400,
body: "Please pass a name on the query string or in the request body"
};
}
context.done();
};
手元の環境からFunctionsを呼んでみる
つぎに手元の環境でFunctionsを読んでみるための準備をします。
ちょっとルーズな実装ですが、POSTメソッドで渡すJSONをファイルに書き出しておき、それをcurlの引数として渡します。
そして、結果として取得できたJSONデータを引数用のJSONファイルに書き出しておきます。同じファイルをリクエストと結果のJSONの格納用に使い回す形になります。
#!/bin/sh
api='https://hello-azure-func-001.azurewebsites.net/api/sample001?code=...'
if [ ! -z "$1" ]; then
cat <<_EOF > arg.json
{"json":{"cmd":"`basename $0 .sh`","text":`echo $1`}}
_EOF
else
[ ! -f arg.json ] && echo '[""]' > arg.json
echo "{\"json\":{\"cmd\":\"`basename $0 .sh`\",\"text\":`cat arg.json`}}" > arg.json
fi
curl -s -X POST -H "Content-Type: application/json" $api -d @arg.json | jq '.json.text' | tee result.json
cp arg.json arg.json.bkup
[ -f result.json ] && mv result.json arg.json
そして、上記のファイルを各テキスト処理コマンドとしてシンボリックリンクにしておきます。これはスクリプトの実行ファイル名をreq.body.json.cmd
の値に設定しているためです。
$ rm cat.sh head.sh nl.sh sort.sh uniq.sh wc.sh
$ ln -s webapi_glue.sh cat.sh
$ ln -s webapi_glue.sh nl.sh
$ ln -s webapi_glue.sh sort.sh
$ ln -s webapi_glue.sh uniq.sh
$ ln -s webapi_glue.sh head.sh
$ ln -s webapi_glue.sh wc.sh
$
$ ls -l *.sh
lrwxr-xr-x 1 fpig staff 14 12 9 05:32 cat.sh -> webapi_glue.sh
lrwxr-xr-x 1 fpig staff 14 12 9 05:33 nl.sh -> webapi_glue.sh
lrwxr-xr-x 1 fpig staff 14 12 9 05:33 sort.sh -> webapi_glue.sh
lrwxr-xr-x 1 fpig staff 14 12 9 05:33 uniq.sh -> webapi_glue.sh
lrwxr-xr-x 1 fpig staff 14 12 9 05:33 head.sh -> webapi_glue.sh
lrwxr-xr-x 1 fpig staff 14 12 9 05:33 wc.sh -> webapi_glue.sh
-rwxr-xr-x 1 fpig staff 565 12 9 05:08 webapi_glue.sh
実行してみる
さっそく実行してみます。シェルコマンドっぽくパイプ記号(|
)を使いたかったのですが、JSONファイルに引数と結果を入れていることもあり、セミコロン(;
)でコマンドをつなげることでそれっぽく見せる感じに留めています...。
とはいえ、実行結果をみると、Functionsで処理されたテキストが順次フィルタされていることが分かります(/* ... */
のコメントは説明上付加したものです)。
こんな感じで、JSONでやりとりする形でAzure Functionをテキスト処理コマンドっぽく扱うことができました。
$ ./cat.sh '["bbb","aaa","eee","ccc","bbb","ddd","fff"]' ; ./sort.sh ; ./uniq.sh ; ./head.sh ; ./nl.sh
[ /* cat.sh の結果 */
"bbb",
"aaa",
"eee",
"ccc",
"bbb",
"ddd",
"fff"
]
[ /* sort.sh の結果。テキストがソートされている */
"aaa",
"bbb",
"bbb",
"ccc",
"ddd",
"eee",
"fff"
]
[ /* uniq.sh の結果。重複していた"bbb"がひとつになっている */
"aaa",
"bbb",
"ccc",
"ddd",
"eee",
"fff"
]
[ /* head.sh の結果。出力が先頭5行に絞られている */
"aaa",
"bbb",
"ccc",
"ddd",
"eee"
]
[ /* nl.sh の結果。行番号が付加されている */
"1 aaa",
"2 bbb",
"3 ccc",
"4 ddd",
"5 eee"
]
まとめ
Azure Fcuntionsを使用してServerLessな環境でテキスト処理コマンドを実現してみました。手元の環境にコマンドがない・インストールしなければならないような状況でもFunctionsとしてコマンドを用意しておくことで準備の手間が省けそうです。
今回はシェルスクリプトからcurlでFunctionsを呼び出していますが、NodeやPythonなどを利用することで、コマンドの実体はFunctions側において置き、クライアントからは単にWebAPIを呼び出すだけ、というより汎用的でServerLessらしい使い方ができそうです。