LoginSignup
2
2

More than 5 years have passed since last update.

テキスト処理コマンドをServerLessにしてみる

Posted at

テキスト処理コマンドを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らしい使い方ができそうです。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2