はじめに
今回の記事では、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-tier
にtrue
を渡す。
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を編集
{
"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をオプショナルにすることができる。
{
"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のデータが格納されてる。
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
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;
{
"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
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;
{
"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
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;
{
"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
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;
{
"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"
}
共通モデュール
モデル
export interface TodoItem {
id?: string;
title: string;
isCompleted: boolean;
}
サービス
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,
});
レポジトリー
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,
});
データベース設定
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,
};
};
カスタムエラー
export class HttpError extends Error {
statusCode;
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
get StatusCode() {
return this.statusCode;
}
}
ユーティリティ
import * as crypto from "crypto";
export const generateId = () => crypto.randomUUID();
その他設定ファイル
{
"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_URI
とCOSMOSDB_PRIMARY_KEY
を環境変数として追加する。COSMOSDB_DATABASE
とCOSMOSDB_CONTAINER
の値はぞれぞれ任意の値を指定する。
{
"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_URI
とCOSMOSDB_PRIMARY_KEY
の値は、azure portalの「CosmosDB リソース => Settgins => Keys」から確認できる。
又は以下のコマンドで取得できる。
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にデータが追加されていることが確認。
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
は実行されない。
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
と置いておく。
{
"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_URI
とCOSMOSDB_PRIMARY_KEY
の組み合わせ。keyの名前の最後に必ず_DOCUMENTDB
を付ける(function.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」から確認できる。
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を中心にマイクロサービスのアプリケーションも簡単に追加していく事できる。