12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Node.js(TypeScript)とAzure FunctionsでサーバレスなREST APIを作り、おまけにCosmosDBのChange Feedを受信してみる。

Last updated at Posted at 2022-10-26

diagram.png

はじめに

今回の記事では、azure functionとComsmosDBを使ってサーバレスなREST APIのアプリケーションを作成する。使用言語はNode.js/TypeScript。さらにデータベースとしてCosmosDBを使っているのでChange FeedのイベントをAzure functionのトリガーを使っておまけに受信してみる。

ソースコード

リソースの作成

リソースグループの作成

az group create \
        --name ${AZ_RESOURCE_GROUP} \
        --location ${AZ_RESOURCE_GROUP_LOCATION}

Azure Function App リソースの作成

Azure Function Appリソースを作成する前に、BLOB、キュー、テーブルストレージをサポートするためのAzure Storage Accountの作成が必要。

az storage account create \
    --name ${AZ_STORAGE_ACCOUNT} \
    --location ${AZ_RESOURCE_GROUP_LOCATION} \
    --resource-group ${AZ_RESOURCE_GROUP} \
    --sku Standard_LRS

上で作成したAzure Storage Accountの名前を--storage-accountにパラメータとして渡し、Azure Function Appのリソースを作成する。

az functionapp create \
    --name ${AZ_FUNCTION} \
    --storage-account ${AZ_STORAGE_ACCOUNT} \
    --consumption-plan-location ${AZ_RESOURCE_GROUP_LOCATION} \
    --resource-group ${AZ_RESOURCE_GROUP} \
    --functions-version 4 \
    --runtime node

CosmosDB リソースの作成

CosmosDB リソースを作成する。デモ又はテスト目的の場合は必ず--enable-free-tiertrueを渡す。

az cosmosdb create \
    --resource-group ${AZ_RESOURCE_GROUP} \
    --name ${AZ_COSMOS_DB} \
    --enable-free-tier true \
    --enable-analytical-storage false

Azure Function ローカルプロジェクトの作成

プロジェクトフォルダーの作成

mkdir az-function-rest-api
cd az-function-rest-api
npm init -y

ライブラリーのインストール

npm install --save-exact @azure/cosmos

開発ライブラリーのインストール

npm install --save-exact -D @azure/functions @types/node azure-functions-core-tools typescript

Azure functionプロジェクトの生成

npx func init --worker-runtime node --language typescript

Http trigger functionの作成

今回は4つfunctionを作成する。

npx func new --name get-todos --template 'HTTP trigger' --authlevel 'anonymous' --language typescript
npx func new --name post-todos --template 'HTTP trigger' --authlevel 'anonymous' --language typescript
npx func new --name delete-todos --template 'HTTP trigger' --authlevel 'anonymous' --language typescript
npx func new --name patch-todos --template 'HTTP trigger' --authlevel 'anonymous' --language typescript

package.jsonを編集

package.json
{
  "name": "az-function-rest-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc", # <= 追加
    "watch": "tsc -w", # <= 追加
    "prestart": "npm run build", # <= 追加
    "start": "func start" # <= 追加
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@azure/cosmos": "3.17.1"
  },
  "devDependencies": {
    "@azure/functions": "3.2.0",
    "@types/node": "18.11.5",
    "azure-functions-core-tools": "4.0.4829",
    "typescript": "4.8.4"
  }
}

現時点でのプロジェクトの構成

.
├── delete-todos
│   ├── function.json
│   └── index.ts
├── get-todos
│   ├── function.json
│   └── index.ts
├── host.json
├── local.settings.json
├── package-lock.json
├── package.json
├── patch-todos
│   ├── function.json
│   └── index.ts
├── post-todos
│   ├── function.json
│   └── index.ts
└── tsconfig.json

Azure functionをローカルで実行

npm run start

ディフォルトでは、それぞれのfunctionのファルダー名がエンドポイントのpathとして自動的に設定される。

❯ npm run start

> az-function-rest-api@1.0.0 prestart
> npm run build


> az-function-rest-api@1.0.0 build
> tsc


> az-function-rest-api@1.0.0 start
> func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.11.2.19273

[2022-10-25T05:54:42.841Z] Worker process started and initialized.

Functions:

        delete-todos: [GET,POST] http://localhost:7071/api/delete-todos

        get-todos: [GET,POST] http://localhost:7071/api/get-todos

        patch-todos: [GET,POST] http://localhost:7071/api/patch-todos

        post-todos: [GET,POST] http://localhost:7071/api/post-todos

実際にHTTP requestを送ってみると、ちゃんとresponseが返ってくる。

curl "http://localhost:7071/api/delete-todos?name=azure"
❯ curl "http://localhost:7071/api/get-todos?name=azure"
Hello, azure. This HTTP triggered function executed successfully.%

エンドポイントのpath、及びHTTP methodはぞれぞれのfunctionのフォルダーに自動生成されているfunction.jsonから設定できる。

  • bindings.methodsのリストに許可したいHTTP methdを指定できる。
  • bidnings.routeでエンドポイントのpathが指定できる。path paramもプレースホルダとして指定でき、functionから参照できるようになる。?をつけることによりpath paramをオプショナルにすることができる。
get-todos/function.json
{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"], # <= postを消去
      "route": "todos/{id?}" # <= 追加
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/get-todos/index.js"
}

path paramを受け取って見るために、get-todo/index.tsを編集する。functionの引数から受け取る事ができるcontextオブジェクトのbindingDataプラパティにpath paramのデータが格納されてる。

get-todos/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    const id = context.bindingData.id; // <= context.bindingData のなかにpath paramのデータが保管されている。

    context.res = {
        body: {
            id
        }
    };

};

export default httpTrigger;

functionをもう一度実行してみると、function.json で設定した内容が反映されていることが確認できる。

npm run start
❯ npm run start

> az-function-rest-api@1.0.0 prestart
> npm run build


> az-function-rest-api@1.0.0 build
> tsc


> az-function-rest-api@1.0.0 start
> func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.11.2.19273

[2022-10-25T06:16:51.255Z] Worker process started and initialized.

Functions:

        delete-todos: [GET,POST] http://localhost:7071/api/delete-todos

        get-todos: [GET] http://localhost:7071/api/todos/{id?} # <= 設定が反映されている。

        patch-todos: [GET,POST] http://localhost:7071/api/patch-todos

        post-todos: [GET,POST] http://localhost:7071/api/post-todos

path paramをrouteに渡して、HTTP requestを送ってみるとしっかりpath paramに乗せたidが返ってくる事が確認できる。

curl -i -X GET http://localhost:7071/api/todos/123
❯ curl -i -X GET http://localhost:7071/api/todos/123
{
  "id": 123
}%

REST APIの作成

完成後のプロジェクト構造

.
├── delete-todos
│   ├── function.json
│   └── index.ts
├── get-todos
│   ├── function.json
│   └── index.ts
├── host.json
├── lib
│   ├── db
│   │   └── db-config.ts
│   ├── dtos
│   │   ├── CreateTodoDto.ts
│   │   └── UpdateTodoDto.ts
│   ├── errors
│   │   └── HttpError.ts
│   ├── models
│   │   └── TodoItem.ts
│   ├── repositories
│   │   └── todo-repository.ts
│   ├── services
│   │   └── todo-service.ts
│   └── utils
│       └── generate-id.ts
├── local.settings.json
├── package-lock.json
├── package.json
├── patch-todos
│   ├── function.json
│   └── index.ts
├── post-todos
│   ├── function.json
│   └── index.ts
└── tsconfig.json

Get todos function

get-todos/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { HttpError } from "../lib/errors/HttpError";
import { todoService } from "../lib/services/todo-service";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const todoId = context.bindingData.id;

  try {
    if (todoId) {
      const result = await todoService.getOne(todoId);
      context.res = {
        status: 200,
        body: result,
      };

      return;
    }
  } catch (err) {
    if (err instanceof HttpError) {
      context.res = {
        status: err.StatusCode,
        body: {
          error: {
            message: err.message,
          },
        },
      };
      return;
    }
  }

  const result = await todoService.getOnes();

  context.res = {
    status: 200,
    body: result,
  };
};

export default httpTrigger;
get-todos/function.json
{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"],
      "route": "todos/{id?}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/get-todos/index.js"
}

Post todos function

post-todos/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { todoService } from "../lib/services/todo-service";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const todoCreateDto = req.body;
  const result = await todoService.createOne(todoCreateDto);

  context.res = {
    status: 201,
    body: result,
  };
};

export default httpTrigger;
post-todos/function.json
{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["post"],
      "route": "todos"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/post-todos/index.js"
}

Delete todos function

delete-todos/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { HttpError } from "../lib/errors/HttpError";
import { todoService } from "../lib/services/todo-service";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const todoId = context.bindingData.id;

  try {
    await todoService.deleteOne(todoId);
  } catch (err) {
    if (err instanceof HttpError) {
      context.res = {
        status: err.StatusCode,
        body: {
          error: {
            message: err.message,
          },
        },
      };
      return;
    }
  }

  context.res = {
    status: 204,
  };
};

export default httpTrigger;
delete-todos/function.json
{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["delete"],
      "route": "todos/{id}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/delete-todos/index.js"
}

Patch todos function

patch-todos/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { HttpError } from "../lib/errors/HttpError";
import { todoService } from "../lib/services/todo-service";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const todoId = context.bindingData.id;
  const updateTodoDto = req.body;

  try {
    await todoService.updateOne(updateTodoDto, todoId);
  } catch (err) {
    if (err instanceof HttpError) {
      context.res = {
        status: err.StatusCode,
        body: {
          error: {
            message: err.message,
          },
        },
      };
      return;
    }
  }

  context.res = {
    status: 204,
  };
};

export default httpTrigger;
patch-todos/function.json
{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["patch"],
      "route": "todos/{id}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/patch-todos/index.js"
}

共通モデュール

モデル

lib/models/TodoItem.ts
export interface TodoItem {
  id?: string;
  title: string;
  isCompleted: boolean;
}

サービス

lib/services/todo-service.ts
import { CreateTodoDto } from "../dtos/CreateTodoDto";
import { UpdateTodoDto } from "../dtos/UpdateTodoDto";
import { HttpError } from "../errors/HttpError";
import { TodoItem } from "../models/TodoItem";
import { todoRepository } from "../repositories/todo-repository";
import { generateId } from "../utils/generate-id";

const getOnes = async (): Promise<TodoItem[] | any> => {
  return await todoRepository.getOnes();
};

const getOne = async (id: string): Promise<any> => {
  return await todoRepository.getOneById(id);
};

const createOne = async (dto: CreateTodoDto): Promise<TodoItem> => {
  const newTodo: TodoItem = {
    id: generateId(),
    ...dto,
    isCompleted: false,
  };

  return await todoRepository.createOne(newTodo);
};

const deleteOne = async (id: string) => {
  if (!id) {
    throw new HttpError("Todo item id is not provided", 400);
  }

  const existingTodo = await todoRepository.getOneById(id);
  if (!existingTodo) {
    throw new HttpError(`TodoItem(id=${id}) is not found.`, 404);
  }

  todoRepository.removeOneById(existingTodo.id);
};

const updateOne = async (dto: UpdateTodoDto, id: string) => {
  if (!id) {
    throw new HttpError("Todo item id is not provided", 400);
  }

  const existingTodo = await todoRepository.getOneById(id);

  if (!existingTodo) {
    throw new HttpError(`TodoItem(id=${id}) is not found.`, 404);
  }

  if (typeof dto.title !== "undefined") {
    existingTodo.title = dto.title;
  }

  if (typeof dto.isCompleted !== "undefined") {
    existingTodo.isCompleted = dto.isCompleted;
  }

  await todoRepository.update(existingTodo);
};

export const todoService = Object.freeze({
  getOnes,
  getOne,
  createOne,
  deleteOne,
  updateOne,
});

レポジトリー

lib/repositorieds/todo-repository.ts
import { initDB } from "../db/db-config";

const db = initDB();

const getOnes = async () => {
  const { container } = await db;
  const { resources } = await container.items.readAll().fetchAll();
  return resources;
};

const getOneById = async (id: string) => {
  const { container } = await db;
  const { resource } = await container.item(id, id).read();
  return resource;
};

const createOne = async (todo: any) => {
  const { container } = await db;
  const { resource } = await container.items.create(todo);
  return resource;
};

const removeOneById = async (id: string) => {
  const { container } = await db;
  await container.item(id, id).delete();
};

const update = async (todo: any) => {
  const { container } = await db;
  await container.item(todo.id, todo.id).replace(todo);
};

export const todoRepository = Object.freeze({
  getOnes,
  getOneById,
  createOne,
  removeOneById,
  update,
});

データベース設定

lib/db/db-config.ts
import { CosmosClient } from "@azure/cosmos";

const cosmosConfig = {
  endpoint: process.env.COSMOSDB_URI,
  primaryKey: process.env.COSMOSDB_PRIMARY_KEY,
  database: process.env.COSMOSDB_DATABASE,
  container: process.env.COSMOSDB_CONTAINER,
  partitionKey: {
    paths: ["/id"],
  },
};

export const initDB = async () => {
  const cosmosClient = new CosmosClient({
    endpoint: cosmosConfig.endpoint,
    key: cosmosConfig.primaryKey,
  });
  const { database } = await cosmosClient.databases.createIfNotExists({
    id: cosmosConfig.database,
  });

  const { container } = await database.containers.createIfNotExists({
    id: cosmosConfig.container,
    partitionKey: cosmosConfig.partitionKey,
  });

  return {
    cosmosClient,
    database,
    container,
  };
};

カスタムエラー

lib/errors/HttpError.ts
export class HttpError extends Error {
  statusCode;
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }

  get StatusCode() {
    return this.statusCode;
  }
}

ユーティリティ

lib/utils/generate-id.ts
import * as crypto from "crypto";

export const generateId = () => crypto.randomUUID();

その他設定ファイル

host.json
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[3.*, 4.0.0)"
  }

local.settings.jsonで環境変数などを設定できる。CosmosDBに接続するためのCOSMOSDB_URICOSMOSDB_PRIMARY_KEYを環境変数として追加する。COSMOSDB_DATABASECOSMOSDB_CONTAINERの値はぞれぞれ任意の値を指定する。

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "COSMOSDB_URI": "https://<COSMOSDB_RESOURCE_NAME>.documents.azure.com:443/",
    "COSMOSDB_PRIMARY_KEY": "<COSMOSDB_PRIMARY_KEY>",
    "COSMOSDB_DATABASE": "node_azure_functions_db",
    "COSMOSDB_CONTAINER": "todos"
  }
}

COSMOSDB_URICOSMOSDB_PRIMARY_KEYの値は、azure portalの「CosmosDB リソース => Settgins => Keys」から確認できる。
Screen Shot 2022-10-26 at 13.30.43.png

又は以下のコマンドで取得できる。

CosmosDB URI

az cosmosdb show \
    --resource-group ${AZ_RESOURCE_GROUP} \
    --name ${AZ_COSMOS_DB} \
    -o tsv \
    --query "documentEndpoint"

CosmosDB Primary Key

az cosmosdb keys list \
    --resource-group ${AZ_RESOURCE_GROUP} \
    --name ${AZ_COSMOS_DB} \
    --type "keys" \
    -o tsv \
    --query "primaryMasterKey"

REST APIを起動する

npm run start
❯ npm run start

> az-function-rest-api@1.0.0 prestart
> npm run build


> az-function-rest-api@1.0.0 build
> tsc


> az-function-rest-api@1.0.0 start
> func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.11.2.19273

[2022-10-26T02:25:55.113Z] Worker process started and initialized.

Functions:

        delete-todos: [DELETE] http://localhost:7071/api/todos/{id}

        get-todos: [GET] http://localhost:7071/api/todos/{id?}

        patch-todos: [PATCH] http://localhost:7071/api/todos/{id}

        post-todos: [POST] http://localhost:7071/api/todos

実際に、以下のコマンドを叩いて、APIをテストしてみる。

curl -i -X POST -d '{"title":"todo 1"}' http://localhost:7071/api/todos
curl -i -X GET http://localhost:7071/api/todos
curl -i -X GET http://localhost:7071/api/todos/{id}
curl -i -X PATCH -d '{"title":"todo 1 updated"}' http://localhost:7071/api/todos/{id}
curl -i -X DELETE http://localhost:7071/api/todos/{id}

todo itemを追加

❯ curl -i -X POST -d '{"title":"todo 1"}' http://localhost:7071/api/todos
HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
Date: Wed, 26 Oct 2022 02:53:40 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "id": "10cb9c6f-6161-4798-8de5-e213a5e2be1d",
  "title": "todo 1",
  "isCompleted": false,
  "_rid": "8T0VAJe9jpsDAAAAAAAAAA==",
  "_self": "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsDAAAAAAAAAA==/",
  "_etag": "\"c001744e-0000-2300-0000-6358a1350000\"",
  "_attachments": "attachments/",
  "_ts": 1666752821
}%  

todo itemを取得

❯ curl -i -X GET http://localhost:7071/api/todos
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 26 Oct 2022 02:55:41 GMT
Server: Kestrel
Transfer-Encoding: chunked

[
  {
    "id": "10cb9c6f-6161-4798-8de5-e213a5e2be1d",
    "title": "todo 1",
    "isCompleted": false,
    "_rid": "8T0VAJe9jpsDAAAAAAAAAA==",
    "_self": "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsDAAAAAAAAAA==/",
    "_etag": "\"c001744e-0000-2300-0000-6358a1350000\"",
    "_attachments": "attachments/",
    "_ts": 1666752821
  }
]% 

todo itemを編集

❯ curl -i -X PATCH -d '{"title":"todo 1 updated"}' http://localhost:7071/api/todos/10cb9c6f-6161-4798-8de5-e213a5e2be1d
HTTP/1.1 204 No Content
Content-Type: text/plain; charset=utf-8
Date: Wed, 26 Oct 2022 02:56:31 GMT
Server: Kestrel

todo itemをIDで取得

❯ curl -i -X GET http://localhost:7071/api/todos/10cb9c6f-6161-4798-8de5-e213a5e2be1d
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Wed, 26 Oct 2022 02:57:34 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "id": "10cb9c6f-6161-4798-8de5-e213a5e2be1d",
  "title": "todo 1 updated",
  "isCompleted": false,
  "_rid": "8T0VAJe9jpsDAAAAAAAAAA==",
  "_self": "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsDAAAAAAAAAA==/",
  "_etag": "\"c0013f9b-0000-2300-0000-6358a1e00000\"",
  "_attachments": "attachments/",
  "_ts": 1666752992
}%

しっかりazure portal側でもCosmosDBにデータが追加されていることが確認。
Screen Shot 2022-10-26 at 12.01.19.png

Change Feed リスナー functionの作成

npx func new --name change-feed-listener --template 'Azure Cosmos DB trigger' --language typescript
.
├── change-feed-listener # <= 追加
│   ├── function.json
│   └── index.ts
├── delete-todos
│   ├── function.json
│   └── index.ts
├── get-todos
│   ├── function.json
│   └── index.ts
├── host.json
├── lib
│   ├── db
│   │   └── db-config.ts
│   ├── dtos
│   │   ├── CreateTodoDto.ts
│   │   └── UpdateTodoDto.ts
│   ├── errors
│   │   └── HttpError.ts
│   ├── models
│   │   └── TodoItem.ts
│   ├── repositories
│   │   └── todo-repository.ts
│   ├── services
│   │   └── todo-service.ts
│   └── utils
│       └── generate-id.ts
├── local.settings.json
├── package-lock.json
├── package.json
├── patch-todos
│   ├── function.json
│   └── index.ts
├── post-todos
│   ├── function.json
│   └── index.ts
└── tsconfig.json

cosmosDBTrigger functionは接続しているCosmosDBにデータが追加されたり、編集されたりすると実行され、そのレコードのデータが渡される。データの消去時にはcosmosDBTriggerは実行されない。

change-feed-listener/index.ts
import { AzureFunction, Context } from "@azure/functions"

const cosmosDBTrigger: AzureFunction = async function (context: Context, documents: any[]): Promise<void> {
    if (!!documents && documents.length > 0) {
        context.log('Document:', documents);
    }
}

export default cosmosDBTrigger;

connectionStringSettingの値に直接connection stringの値を代入して実行するとエラーになる。実際の値はlocal.settings.jsonに書き込んで、connectionStringSettingの値はCOSMOSDB_CONNECTION_STRING_DOCUMENTDBと置いておく。

change-feed-listener/function.json
{
  "bindings": [
    {
      "type": "cosmosDBTrigger",
      "name": "documents",
      "direction": "in",
      "leaseCollectionName": "leases",
      "connectionStringSetting": "COSMOSDB_CONNECTION_STRING_DOCUMENTDB", # <= local.settings.jsonの値を参照。
      "databaseName": "node_azure_functions_db",
      "collectionName": "todos",
      "createLeaseCollectionIfNotExists": true
    }
  ],
  "scriptFile": "../../dist/src/change-feed-listener/index.js"
}

local.settings.jsonを編集し、COSMOSDB_CONNECTION_STRING_DOCUMENTDBを追加する。COSMOSDB_CONNECTION_STRING_DOCUMENTDBの値は、COSMOSDB_URICOSMOSDB_PRIMARY_KEYの組み合わせ。keyの名前の最後に必ず_DOCUMENTDBを付ける(function.json から値を参照をするために必要)。

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "COSMOSDB_URI": "https://<COSMOSDB_NAME>.documents.azure.com:443/",
    "COSMOSDB_PRIMARY_KEY": "<COSMOSDB_PRIMARY_KEY>",
    "COSMOSDB_DATABASE": "node_azure_functions_db",
    "COSMOSDB_CONTAINER": "todos"
    # 追加
    "COSMOSDB_CONNECTION_STRING_DOCUMENTDB": "AccountEndpoint=https://<COSMOSDB_NAME>.documents.azure.com:443/;AccountKey=<COSMOSDB_PRIMARY_KEY>;"
  }
}

又はazure portalの「CosmosDB リソース => Settgins => Keys」から確認できる。
Screen Shot 2022-10-26 at 13.31.09.png

npm run start

新しくchange-feed-listener: cosmosDBTriggerがfunctionとして追加されている。

❯ npm run start

> az-function-rest-api@1.0.0 prestart
> npm run build


> az-function-rest-api@1.0.0 build
> tsc


> az-function-rest-api@1.0.0 start
> func start


Azure Functions Core Tools
Core Tools Version:       4.0.4829 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.11.2.19273

[2022-10-26T05:15:06.386Z] Worker process started and initialized.

Functions:

        delete-todos: [DELETE] http://localhost:7071/api/todos/{id}

        get-todos: [GET] http://localhost:7071/api/todos/{id?}

        patch-todos: [PATCH] http://localhost:7071/api/todos/{id}

        post-todos: [POST] http://localhost:7071/api/todos

        change-feed-listener: cosmosDBTrigger # <= 追加

実際にデータを追加してみると。

❯ curl -i -X POST -d '{"title":"todo 1"}' http://localhost:7071/api/todos
HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
Date: Wed, 26 Oct 2022 05:40:11 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "id": "29c84398-6b37-4dfd-ad8b-45403a62f4d7",
  "title": "todo 1",
  "isCompleted": false,
  "_rid": "8T0VAJe9jpsEAAAAAAAAAA==",
  "_self": "dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsEAAAAAAAAAA==/",
  "_etag": "\"cf01af89-0000-2300-0000-6358c83b0000\"",
  "_attachments": "attachments/",
  "_ts": 1666762811
}%

change-feed-listener のログにchange feedから受け取ったデータが出力され、change-feed-listener functionが無事に実行されたことが確認できる。

[2022-10-26T05:40:16.500Z] Executing 'Functions.change-feed-listener' (Reason='New changes on collection todos at 2022-10-26T05:40:16.4919090Z', Id=50591fbf-9ef8-49cc-b34a-abbdca3631d1)
[2022-10-26T05:40:16.519Z] Document: [
[2022-10-26T05:40:16.519Z]   {
[2022-10-26T05:40:16.519Z]     id: '29c84398-6b37-4dfd-ad8b-45403a62f4d7',
[2022-10-26T05:40:16.519Z]     _rid: '8T0VAJe9jpsEAAAAAAAAAA==',
[2022-10-26T05:40:16.519Z]     _self: 'dbs/8T0VAA==/colls/8T0VAJe9jps=/docs/8T0VAJe9jpsEAAAAAAAAAA==/',
[2022-10-26T05:40:16.519Z]     _ts: 1666762811,
[2022-10-26T05:40:16.519Z]     _etag: '"cf01af89-0000-2300-0000-6358c83b0000"',
[2022-10-26T05:40:16.519Z]     title: 'todo 1',
[2022-10-26T05:40:16.519Z]     isCompleted: false,
[2022-10-26T05:40:16.519Z]     _lsn: 11
[2022-10-26T05:40:16.519Z]   }
[2022-10-26T05:40:16.519Z] ]

Functionsのデプロイメント

プロジェクトのルートディレクトリーで以下のコマンドを実行するだけで、簡単にデプロイメントができる。

npx func azure functionapp publish ${AZ_FUNCTION}
❯ npx func azure functionapp publish myTestFunction231917910
Setting Functions site property 'netFrameworkVersion' to 'v6.0'
Getting site publishing info...
Creating archive for current directory...
Uploading 1.61 MB [#########################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions in myTestFunction231917910:
    change-feed-listener - [cosmosDBTrigger]

    delete-todos - [httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos/{id}

    get-todos - [httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos/{id?}

    patch-todos - [httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos/{id}

    post-todos - [httpTrigger]
        Invoke url: https://mytestfunction231917910.azurewebsites.net/api/todos

以下のコマンドでデプロイメントされたfunctionのログを確認することができる。

npx func azure functionapp logstream ${AZ_FUNCTION}

おわり

Node.js(TypeScript)とAzure FunctionsでサーバレスなREST APIのアプリケーションの作成ができた。azure functionを使えば高速にアプリケーションの開発ができる。デプロイメントもとても簡単。CosmosDBのChange feedも簡単に受信することができるので、azure functionを中心にマイクロサービスのアプリケーションも簡単に追加していく事できる。

12
9
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
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?