LoginSignup
0
0

Azure Functions Node.js Programming Modelでv3→4に移行してみた

Posted at

きっかけ

個人開発で以下の 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のみ取り扱うことを想定し、その説明上で重要なファイルの中身だけ以下の通り仮定します。

healthcheck/function.json
{
  "bindings": [
    {
      "authLevel": "Function",
      "type": "HttpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get", "post"]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/healthcheck/index.js"
}
healthcheck/index.ts
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,
  };
}
package.json
{
  "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"
  }
}
tsconfig.json
{
  "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 ファイルを修正します。修正のポイントも示しておきます。

src/functions/healthcheck.ts
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 した方が良いです。

src/functions/index.ts
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の型定義を参照してください。
  • v3 では API のパスが function.json が存在する相対パスにそのまま対応していましたが、v4 ではapp.http関数の第 1 引数に API のパスを設定するように修正します。
  • v3 で function.json に設定していた メソッド、認証形式を、それそれapp.http関数の methods、authLevel プロパティに設定し、src/functions/healthcheck.ts で export したヘルスチェックもどきの動作を行う async function を handler プロパティに設定するように修正します。
package.json
{
  "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 の移行にあたる、サンプルコード修正のコミットです。

0
0
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
0
0