Azure Functions: JavaScript関数でPythonを動かす

# はじめに
今回は、Azure FunctionsのJavaScript関数でPythonスクリプトを動かす方法を検証します。
Azure FunctionsにはPython関数(プレビュー)もありますが、JavaScript関数を開発しながらちょっとPythonを動かしたいと思うこともあり、試しということでやってみます。

# どうやるか
Azure Functionsでは、Pythonの拡張機能が用意されています。このPythonはFunctions Appの関数として直接使用することはできませんが、コマンドとして使うことはできます。
また、npmにはPythonスクリプトを実行するためのモジュール「python-shell」があり、JavaScriptから外部プロセスとしてPythonスクリプトを実行し、値のやり取りをすることができます。
これらを組み合わせ、Azure Functions上でPythonのスクリプトを実行してみます。
![afpj-0101.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/eea17a06-4a66-7b33-6d0f-08e2d2f5829b.png)

# Python拡張機能のインストール
**※ 以下、AzureにJavaScriptランタイムのFunctions Appが作成されている前提での検証となります。**
Functions AppにPython (今回は3.6.4)をインストールします。
AzureポータルでPythonをインストールするFunctions Appに移動し、「プラットフォーム機能」の「高度なツール(Kudu)」からKuduに移動します。
![afpj-0102.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/c05cfba0-d915-5bee-10b1-2ad280050b8d.png)
Kuduの「Site extensions」で「Gallery」タブから32bit版の「Python 3.6.4 x86」を探し、＋ボタンをクリックしてインストールします。
![afpj-0103.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/419530fc-d223-e4e1-1be0-4b1688f4ab6b.png)
しばらく経つと「Site extensions」の「Installed」タブにインストールした拡張機能が表示されます。説明欄に「python.exe」の場所が記載されているので、コピーして控えておきます。
![afpj-0104.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/75bc5828-d9b8-c15b-fb14-31495dd00348.png)

# 関数プロジェクトとPython仮想環境の作成
関数の作成はVisual Studio Code(VS Code)で行います。関数のローカルデバッグを行うため、Python仮想環境も一緒に作成します。
VS Codeを開き、Azure Functions拡張機能で新しいJavaScriptの関数プロジェクトを作成します。
![afpj-0105.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/e9b6d39e-0160-2d26-b75a-6db0a85d85e4.png)
ターミナルを開き、Pythonの「venv」で関数プロジェクトの「.env」内にPythonの仮想環境を作成します。
![afpj-0106.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/c81d1ecb-78b7-ce77-2650-5ca0b05ba50d.png)
作成した仮想環境がFunctions App上にデプロイされないように「.funcignore」にPython仮想環境のパスを追加しておきます。
![afpj-0107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/41c95a8f-a8f8-b5de-6320-3c3ff9668801.png)

# 関数の作成
関数プロジェクトにHTTPトリガーの関数を作成します。
ターミナルでPythonスクリプトを動かす関数のディレクトリに移動して、npmの初期化と「python-shell」のインストールを行います。

```sh
npm init -y
npm i --save python-shell
```

リクエストを受ける関数の「index.js」を下記コードに書き換えます。
スクリプトは、HTTPトリガーのリクエストデータを「python-shell」でPythonスクリプト「test.py」の標準入力に流し、Pythonスクリプトの処理結果を標準出力から取り出してレスポンスするようになっています。
Pythonスクリプトを実行する「python.exe」は、ローカルとFunctions Appでパスが異なるため、Functions Appにデプロイされない「local.setting.json」の有無で環境を判別してパスを設定しています。

```js
/* モジュール読み込み */
const { PythonShell } = require("python-shell")
const fs = require("fs")
const path = require("path")

module.exports = async function (context, req) {

    // 実行するpyファイルの設定
    var scriptName = "test.py"

    // pyスクリプトを実行
    var result = await executePy(context, scriptName, req)

    // 結果の出力
    context.res = {
        body: result
    };

};


/* Pythonの実行 */
// python.exeの場所指定
var pythonPath
if (fs.existsSync(path.resolve(__dirname, "../local.settings.json"))) {
    // ローカルデバッグのPython仮想環境
    pythonPath = path.resolve(__dirname, "../.env/Scripts/python.exe")
}
else {
    // Functions AppのPython拡張機能
    pythonPath = "D:\\home\\python364x86\\python.exe" 
}

async function executePy(context, scriptName, inputData) {

    return new Promise((resolve, reject) => {
        
        // PythonShellの諸設定
        var options = {
            "mode": "text",
            "pythonPath": pythonPath,
            "scriptPath": __dirname
        };

        // PythonShell
        var pyScript = new PythonShell(scriptName, options)

        // Pythonにデータ送信
        pyScript.send(JSON.stringify(inputData))

        // 結果の受信
        var result
        pyScript.on('message', function (message) {
            context.log(message);
            result = message
        });

        // スクリプトの終了
        pyScript.end(function (err, code, signal) {
            if (err) {
                reject(err)
            }
            else {
                resolve(result)
            }
        });
    })
}
```

「index.js」と同じディレクトリ内に実行するPythonスクリプト「test.py」を作成し、下記コードに書き換えます。
Pythonスクリプトでの処理は、リクエスト本文、またはリクエストパラメータの「name」キーを読み取って、「Hello {name}」を返すという単純なものとしています。

```py
# coding: utf-8

import sys
import json

# JavaScriptからのデータを入力
req = json.loads(sys.stdin.readline())

if "body" in req and "name" in req["body"]:
    name = req["body"]["name"]
elif "query" in req and "name" in req["query"]:
    name = req["query"]["name"]
else: 
    name = "World"

print(f"Hello {name}")
```
![afpj-0108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/9d62bf68-1fa2-bc1e-0679-07b22abbe666.png)
# テスト
関数が完成したら、VS Codeでローカルデバッグをします。
VS CodeでF5キーを押してデバッグモードを起動し、Azure Functions Core Toolsの起動後に表示されるローカルホストのURLをコピーし、URLをPostmanでリクエストします。
リクエストパラメータの「name」に「Azure」を付加してリクエストすると、「Hello Azure」というレスポンスが得られました。
![afpj-0109.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/8c9bcfc8-f2f4-aedd-e16c-8c431c36d5d1.png)
ローカルでの動作が確認できたので、関数をFunctions Appにデプロイします。
VS CodeのAzure Functions拡張機能でPython拡張機能をインストールしたFunctions Appに対してデプロイを実行します。
![afpj-0110.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/8fd17e3f-25d5-c763-b8c4-e400d7bbc195.png)
Azureポータルでデプロイした関数のURLを取得します。
![afpj-0111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/6f086f24-88e3-5167-000f-f1e0af427256.png)
ローカルデバッグと同様に、Functions Appの関数に対してPostmanでリクエストします。
リクエストパラメータの「name」に「Azure」を付加してリクエストすると、「Hello Azure」というレスポンスが得られ、Functions App上でも正常にPythonスクリプトが実行されました。
![afpj-0112.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322093/29c9c2c3-5254-c269-453e-5550fe1e2458.png)

# まとめ
Azure FunctionsのJavaScript関数でPythonスクリプトを動かすというのは思いのほか簡単にできることがわかりました。
「python-shell」モジュールによりPythonスクリプトの書き方には制限が出てしまいますが、ラッパーをかませるなどすればその問題も解決できそうです。
JavaScript関数であればDurable Functionsも使えるので、この仕組みと組み合わせてみるのも面白そうです。