かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Azure Functions を TypeScript でやってみた「ハローワールドから Durable Functions まで」

公式のブログはこちら。

azure.microsoft.com

2019/02/25 くらいに出た Azure Functions Core Tools の 2.4.401 のリリースノートに

- Add TypeScript templates

の文言が。

これは試すしかないでしょう。ということでアップデートして func init してみます。

C:\Users\kaota\Documents\Labs\TSFunc\Hello>func init
Select a worker runtime: node
Select a Language:
javascript
typescript

typescript がちゃんとある。typescript を選択すると package.json には以下のような devDependencies が追加されて

  "devDependencies": {
    "@azure/functions": "^1.0.1-beta1",
    "typescript": "^3.3.3"
  }

さらには tsconfig.json も生成されます。

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "outDir": "dist",
    "rootDir": ".",
    "sourceMap": true,
    "strict": false
  }
}

npm install しておきます。

func new で HttpTrigger を選ぶとちゃんと index.ts が追加されました。

import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    context.log('HTTP trigger function processed a request.');
    const name = (req.query.name || (req.body && req.body.name));

    if (name) {
        context.res = {
            // status: 200, /* Defaults to 200 */
            body: "Hello " + (req.query.name || req.body.name)
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
};

export default httpTrigger;

そして生成された HttpTrigger の function.json を見てみるとこうなってます。

{
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "authLevel": null,
      "type": "http",
      "direction": "out",
      "name": "res",
      "methods": null
    }
  ],
  "scriptFile": "../dist/Hello/index.js"
}

なるほど、scriptFile で dist フォルダーの下に作られるコンパイル後の JavaScript を見るようにしてるんですね。ということで npm run build をするか、npm run watch をしてビルドをします。

そうすると dist フォルダーに js が生成されます。あとは func host start をするとローカルで動くのでアクセスしてみましょう。

動いた!!

f:id:okazuki:20190228130209p:plain

Durable Functions も試してみる

ハローワールド動いたので次は、個人的に Azure Functions のキラーコンテンツだと思ってる Durable Functions を試してみます。これが動かないと始まらない。 とりあえず以下のページを参考に…

github.com

docs.microsoft.com

まず下準備

func extensions install -p Microsoft.Azure.WebJobs.Extensions.DurableTask -v 1.7.0
npm install durable-functions

node_modules の下の durable-functions を見てみたらちゃんと .d.ts もありますね!

f:id:okazuki:20190228130712p:plain

func new にも、ちゃんと Durable 系のテンプレートが入ってます。いいね。

f:id:okazuki:20190228130831p:plain

ということで Starter と Orchestrator と Activity を適当に作ります。

f:id:okazuki:20190228131002p:plain

インテリセンス出る。まじ神

f:id:okazuki:20190228131236p:plain

ということで、こんな感じに Starter を調整して

import * as df from "durable-functions"
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpStart: AzureFunction = async function (context: Context, req: HttpRequest): Promise<any> {
    const client = df.getClient(context);
    const instanceId = await client.startNew('Orchestrator', undefined, req.body);

    context.log(`Started orchestration with ID = '${instanceId}'.`);

    return client.createCheckStatusResponse(context.bindingData.req, instanceId);
};

export default httpStart;

Orchestrator もこんな感じに

import * as df from "durable-functions"

const orchestrator = df.orchestrator(function* (context) {
    const outputs = [];

    outputs.push(yield context.df.callActivity("Activity1", "Tokyo"));
    outputs.push(yield context.df.callActivity("Activity2", "Seattle"));

    return outputs;
});

export default orchestrator;

Activity 関数はこんな感じ。

import { AzureFunction, Context } from "@azure/functions"

const activityFunction: AzureFunction = async function (context: Context): Promise<string> {
    return `Hello ${context.bindings.name} from Activity1!`;
};

export default activityFunction;

ではビルドしたら実行しようと思ったのですが以下のエラーが。

node_modules/durable-functions/lib/src/utils.d.ts:1:23 - error TS2688: Cannot find type definition file for 'node'.
node_modules/durable-functions/lib/src/utils.d.ts:8:56 - error TS2503: Cannot find namespace 'NodeJS'.

dist に js は生成されてるからビルドは出来てるけど気持ち悪いのでとりあえず以下のコマンドをうったら消えた。

npm i -d @types/node

Azure Storage Emulator を起動して func host start します。

Azure Storage Emulator は最新にしましょう。少し古いやつだと何か問題が起きた記憶。

そうするとエラーが出ました。

[2019/02/28 4:20:44] A host error has occurred
[2019/02/28 4:20:44] Microsoft.WindowsAzure.Storage: Settings must be of the form "name=value".
Settings must be of the form "name=value".

そういえば local.settings.json に Azure Storage の接続文字列が初期状態では設定されてないんでした。エミュレーター用の接続文字列をとりあえず追加します。

こんな感じで

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true"
  }
}

あ、あと func host start 以外にも npm run startnpm run start:host とかでも OK。 何やってるかは package.json にあります。

起動したら Starter のエンドポイントを叩くとばっちり動きます。

f:id:okazuki:20190228132921p:plain

statusQueryGetUri を叩くと以下の JSON が返ってきました。

{
  "instanceId":"28038e30463544ab965db44f9c9ccb1e",
  "runtimeStatus":"Completed",
  "input":null,
  "customStatus":null,
  "output":["Hello Tokyo from Activity1!","Hello Seattle from Activity2!"],
  "createdTime":"2019-02-28T04:28:54Z",
  "lastUpdatedTime":"2019-02-28T04:28:56Z"
}

ばっちりですね。適当に Azure に Function App を作ってデプロイもしてみました。とりあえず試すだけなら以下のコマンドで

func azure functionapp publish <作った Function App の名前>

デプロイされました。関数もちゃんと認識されてます。

f:id:okazuki:20190228133603p:plain

叩いてみると無事動いた動いた。めでたい。

{
  "instanceId":"03e27dc7db234991bead2ea369173963",
  "runtimeStatus":"Completed",
  "input":null,
  "customStatus":null,
  "output":["Hello Tokyo from Activity1!","Hello Seattle from Activity2!"],
  "createdTime":"2019-02-28T04:36:16Z",
  "lastUpdatedTime":"2019-02-28T04:36:26Z"
}

まとめ

これで Azure Functions の node のほうで開発するときに手軽に TypeScript 始めることが出来るようになりました。 TypeScript 好きな人も是非 Azure Functions 使ってみてね!