きっかけ
個人開発で以下の Azure Functions を開発・デプロイし、しばらくの間 GitHub で塩漬けしていました。
名称 | 値 |
---|---|
OS | Windows |
プラン | Consumption |
リージョン | Japan East |
ランタイムのバージョン | 4.x |
関数言語 | Node.js(Typescript) v18.x |
Azure Functions Node.js Programming Modelのバージョン | 3.5.1 |
そんな中、2023 年 9 月辺りに、Dependabotから、Azure Functions Node.js Programming Model の 3.5.1→4.0.1 アップデートの Pull Request が届いていました。
公式ブログを調べたところ、ちょうどそのタイミングで Azure Functions Node.js Programming Model の v4 が GA されたようでした。
この公式ブログと公式リファレンスを参考にし、Azure Functions Node.js Programming Model の v3→4 に移行した際の Tips を記しておきます。
公式で注意喚起していますが、Azure Functions のランタイムのバージョンと、Azure Functions Node.js Programming Model のバージョンとは互いに別物です。
両方ともメジャーバージョンが 4 ですが、これは偶然…らしいです。
当記事では後者の Azure Functions Node.js Programming Model の方をバージョンアップしたケースとなります。
v4 の特徴
何と言っても、 function.json が不要になることです。
v3 以前では、1 つの Azure Functions に複数個デプロイできる関数は、以下のファイルをすべてセットにしていました。
- ソースコード群(C#、Javascript、Python、...)
- function.json
function.json の記述がソースコード化されたため、ソースコードのファイル管理がシンプルになり、特に Durable Functions で、より柔軟にソースコードを記載することが期待できます。
対象
Azure Functions Core Toolsといった何かしらの方法で自動生成した直後である、以下のディレクトリ・ファイル構成を仮定します。
FunctionsTSV4
├─ healthcheck
│ ├─ function.json
│ └─ index.ts
├─ ...
:
├─ host.json
├─ local.settings.json
├─ package.json
├─ package-lock.json
└─ tsconfig.json
関数が複数個存在する前提ですが、今後の説明のため、GET メソッドの HTTP トリガーであるヘルスチェックもどきの関数healthcheck
のみ取り扱うことを想定し、その説明上で重要なファイルの中身だけ以下の通り仮定します。
{
"bindings": [
{
"authLevel": "Function",
"type": "HttpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/healthcheck/index.js"
}
import { Context, HttpRequest } from "@azure/functions";
export default 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);
const responseMessage = name
? "Hello, " + name + ". This HTTP triggered function executed successfully."
: "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.";
context.res = {
status: 200,
body: responseMessage,
};
}
{
"name": "functions-ts-v4",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"clean": "rimraf dist",
"prestart": "npm run clean && npm run build",
"start": "func start"
},
"dependencies": {
"@azure/functions": "^3.5.1"
},
"devDependencies": {
"@types/node": "^18.19.5",
"azure-functions-core-tools": "^4.0.5455",
"rimraf": "^5.0.5",
"typescript": "^5.3.3"
}
}
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "dist",
"rootDir": ".",
"sourceMap": true,
"strict": false
}
}
移行
Azure Functions Node.js Programming Model の v4 でContext
の型定義が削除されたため、単に以下のコマンドを実行し、Azure Functions Node.js Programming Model の npm パッケージである@azure/functions
を v3→4 へアップグレードしただけでは NG です。
npm i @azure/functions@latest
実際に上記コマンドの実行のみでは、npm run build
実行時に以下のコンパイルエラーが発生してしまいます。
Module '"@azure/functions"' has no exported member 'Context'.ts(2305)
公式リファレンスでの説明より、v4 でソースコードレベルで破壊的変更が入ったため、上記コマンドの実行の他に、ソースコードの修正も必要です。
具体的にはまず、以下の通りディレクトリ・ファイルを再構成します。
FunctionsTSV4
├─ host.json
├─ local.settings.json
├─ package.json
├─ package-lock.json
├─ src
│ └─ functions
│ ├─ healthcheck.ts
│ ├─ ...
: :
│ └─ index.ts
└─ tsconfig.json
その後、以下の 3 ファイルを修正します。修正のポイントも示しておきます。
import {
HttpRequest,
HttpResponseInit,
InvocationContext,
} from "@azure/functions";
export default async function (
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
context.log("HTTP trigger function processed a request.");
const body = await request.text();
const name =
request.query.get("name") || (body !== "" && JSON.parse(body).name);
const responseMessage = name
? "Hello, " + name + ". This HTTP triggered function executed successfully."
: "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.";
return {
status: 200,
body: responseMessage,
};
}
- v3 での healthcheck/index.ts に対応しています。
- v3 では引数を左からコンテキスト → リクエストの順で定義しましたが、v4 ではリクエスト → コンテキストの順で定義するように修正します。
- v3 でリクエストのボディ・ヘッダー・クエリパラメーターを取得する方法がJavascript 標準の fetch 関数もどきに変更されたため、それを修正します。
- リクエストボディの文字列を
request.text()
を実行して Promise を取得し、それが JSON 形式である場合のみ parse しています。
- リクエストボディの文字列を
-
@azure/functions
から import する引数のコンテキストの型を、Context
からInvocationContext
へと変更されました。そのため、v3 でレスポンスを明示的に返す場合context.res
に設定するか return するかのいずれかでしたが、v4 ではcontext.res
に設定できなくなり、必ず return するように修正します。
上記ソースコードだとHttpRequest.text
関数を実行してリクエストボディを取得していますが、HttpRequest
の型定義を見ると、unknown 型の Promise を返すHttpRequest.json
関数が存在し、一見この関数を実行した方がシンプルのように見えます。
しかし、HttpRequest.json
関数の内部処理の仕様が、リクエストボディが JSON 形式であろうがなかろうが関係なく問答無用で parse するとなっているため、JSON 形式でないリクエストボディを設定して API を実行すると実行時例外が発生します。
リクエストボディが JSON 形式であることが確約される場合はHttpRequest.json
関数を採用しても良いですが、確約されない場合はHttpRequest.text
関数を実行して文字列として取得し、その後 JSON 形式かのバリデーションチェックを挟んでJSON.parse
関数で parse した方が良いです。
import { app } from "@azure/functions";
import healthcheck from "./healthcheck";
// import ...
app.http("healthcheck", {
methods: ["GET", "POST"],
authLevel: "function",
handler: healthcheck,
});
// 以降、healthcheck以外の関数を同様に定義する
// HTTPトリガーの場合
// app.http("...", {
// :
// });
// タイマートリガーの場合
// app.timer("...", {
// schedule: "cron式",
// handler: ...,
// });
- v3 での healthcheck/function.json に対応しています。
- v3 では HTTP トリガーの設定を bindings > type(direction が
in
) にhttpTrigger
を設定して実現していましたが、v4 では@azure/functions
から import できるapp
を用いて、app.http
関数を呼び出すように修正します。- 複数個の HTTP トリガーの関数を定義したい場合は、そのまま
app.http
関数を続けて呼び出せば良いです。 - HTTP トリガー以外でも、例えばタイマートリガーの場合は
app.timer
関数を呼び出せば良いです。詳細はapp
の型定義を参照してください。
- 複数個の HTTP トリガーの関数を定義したい場合は、そのまま
- v3 では API のパスが function.json が存在する相対パスにそのまま対応していましたが、v4 では
app.http
関数の第 1 引数に API のパスを設定するように修正します。 - v3 で function.json に設定していた メソッド、認証形式を、それそれ
app.http
関数の methods、authLevel プロパティに設定し、src/functions/healthcheck.ts で export したヘルスチェックもどきの動作を行う async function を handler プロパティに設定するように修正します。
{
"name": "functions-ts-v4",
"version": "1.0.0",
"main": "dist/src/functions/*.js",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"clean": "rimraf dist",
"prestart": "npm run clean && npm run build",
"start": "func start"
},
"dependencies": {
"@azure/functions": "^4.1.0"
},
"devDependencies": {
"@types/node": "^18.19.5",
"azure-functions-core-tools": "^4.0.5455",
"rimraf": "^5.0.5",
"typescript": "^5.3.3"
}
}
- 関数を構成する.ts ファイルをビルドによってトランスパイルした全.js ファイルが該当するようにGLOBを組み、その値を持つ main プロパティを追記します。
-
npm run build
実行時に、src/functions 配下の各.ts ファイルをトランスパイルした.js ファイルが dist/src/functions 配下にそれぞれ生成するため、main プロパティにdist/src/functions/*.js
を追記しています。
-
tsconfig.json の修正は必要ありません。
なお、上記で src/functions/healthcheck.ts で export した関数を src/functions/index.ts で import し、handler プロパティに設定していましたが、以下の通り import せずに handler プロパティに匿名関数を直接設定することもできます。
import {
app,
HttpRequest,
HttpResponseInit,
InvocationContext,
} from "@azure/functions";
app.http("healthcheck", {
methods: ["GET", "POST"],
authLevel: "function",
// importせず、匿名関数を直接設定
handler: async function (
request: HttpRequest,
context: InvocationContext
): Promise<HttpResponseInit> {
// 省略
return { status: 200 };
},
});
しかし、その匿名関数に対するユニットテストを実装する場合などを踏まえ、export しておいた方がお行儀が良いです。
補足
↑Azure Functions Node.js Programming Model の v3→4 の移行にあたる、サンプルコード修正のコミットです。