TypeScriptを使用したAzure Functions: AzuriteでBlobトリガーを動かす

2024年9月28日

この実験では、TypeScriptでAzure Functionsを開発し、Azuriteを使用してBlobトリガーの動作をローカルでテストします。これにより、Azure Blob Storageでファイル作成したら動作する関数の開発をローカルで作成することができ、クラウド環境に依存せずに行うことができるようになります。

/Users/takumi/Documents/src/private_src/azure/functions/azfunc-samples/functions_with_azuriteで作業します。

今回の実験で作ったリポジトリはこちらです。

https://github.com/xiaotiantakumi/az-samples/tree/main/functions-with-azurite

Azuriteで動作させるので、今回はコンテナー (ポーリング)で作成しています。

トリガーの違いについてはこちらを参照してください。

https://learn.microsoft.com/ja-jp/azure/azure-functions/storage-considerations?tabs=azure-cli#trigger-on-a-blob-container

Azure Functions の Azure Blob Storage トリガーの推奨としては、Event Gridを使ったイベント ベースのトリガーがあります。

少し宣伝させてください!Azureの試験対策本を執筆しました。

Blob Storageのイベントに反応する関数を作成

バージョン確認など

$ func –version 4.0.5455

  • プロジェクト初期化時に、Node.jsランタイムとTypeScriptを選択します。
takumi@ ~/Documents/src/private_src/daily/2024-09-16/functions_with_azrite$ func init
Select a number for worker runtime:
1. dotnet
2. dotnet (isolated process)
3. node
4. python
5. powershell
6. custom
Choose option: 3
node
Select a number for language:
1. javascript
2. typescript
Choose option: 2
typescript
The new Node.js programming model is generally available. Learn more at https://aka.ms/AzFuncNodeV4
Writing package.json
Writing .funcignore
Writing tsconfig.json
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/takumi/Documents/src/private_src/daily/2024-09-16/functions_with_azrite/.vscode/extensions.json
Running 'npm install'...%   

func new

この部分では、Azure Functions CLIを使用して新しい関数を作成しています。「func new」コマンドを実行し、利用可能なトリガーテンプレートのリストから選択しています。

Azure Blob Storageトリガーを選択し、新しい関数「storageBlobTrigger」を作成しました。これにより、Blob Storageのイベントに反応する関数が生成されました。

takumi@ ~/Documents/src/private_src/daily/2024-09-16/functions_with_azrite$ func new 
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Durable Functions entity
4. Durable Functions orchestrator
5. Azure Event Grid trigger
6. Azure Event Hub trigger
7. HTTP trigger
8. Azure Queue Storage trigger
9. Azure Service Bus Queue trigger
10. Azure Service Bus Topic trigger
11. Timer trigger
Choose option: 1
Azure Blob Storage trigger
Function name: [storageBlobTrigger] 
Creating a new file /Users/takumi/Documents/src/private_src/daily/2024-09-16/functions_with_azrite/src/functions/storageBlobTrigger.ts
The function "storageBlobTrigger" was created successfully from the "Azure Blob Storage trigger" template.

コードの内容

ここからが今回のメインの話になります。

src/functions/storageBlobTrigger.ts

このコードは、Azure FunctionsでBlob Storageからトリガーを受け取り、それに基づいて処理を行う「Storage Blob Trigger」の実装です。特定のBlobがアップロードや更新された際にこの関数が実行され、Blobデータを受け取ってログに記録します

import { app, InvocationContext } from '@azure/functions';

export async function storageBlobTrigger(
  blob: Buffer,
  context: InvocationContext
): Promise<void> {
  context.log(
    `Storage blob function processed blob "${context.triggerMetadata.name}" with size ${blob.length} bytes`
  );
}

app.storageBlob('storageBlobTrigger', {
  path: 'work/{name}',
  connection: '',
  handler: storageBlobTrigger,
});

local.settings.jsonにfunctionsで使用するの環境変数を設定します。

AzureWebJobsStorageにAzuriteの接続文字列を設定します。
Azure Functions では、Blob、Queue、Table などのストレージサービスを使用するトリガーやバインディングを設定することができます。この場合、関数がデータにアクセスするためには、Storage Accountに接続する必要があります。AzureWebJobsStorage は、このストレージアカウントの接続情報を提供します。

Azuriteを使う場合、ローカルに対する接続文字列は決まっています。

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "AzureWebJobsStorage": "AccountName=devstoreaccount1;AccountKey=xxx;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;",
    "Hoge": "AccountName=devstoreaccount1;AccountKey=xxx;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;"
  }
}

ここでのポイントはmy_azuriteという部分です。my_azuriteは、Dockerコンテナ内でAzuriteを動かす際に、Dockerのカスタムネットワークでホスト名として指定されたものとなります。この設定により、他のコンテナからmy_azuriteでアクセス可能です。一般的には、接続文字列のエンドポイントはhttp://127.0.0.1:10000/となります。
こちらを参考にしてください。
https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#http-connection-strings

storageBlobの設定について掘り下げる

下記の設定について掘り下げてみていきたいと思います。
handlerは今回は固定となります。pathとconnectionについて深掘りします。

app.storageBlob('storageBlobTrigger', {
  path: 'work/{name}',
  connection: '',
  handler: storageBlobTrigger,
});

まずは動作確認

実際に動作させるために、Microsoft Azure Storage Explorerを使用します。
別記事でMicrosoft Azure Storage Explorerについて書いてます。

とりあえず、workとhogeというコンテナを作成しました。

準備ができたら、workというコンテナーを作成し、その中に任意のファイルをアップロードします。

VSCodeでデバッグ実行してみましょう。ブレークポイントを置いて待ち構えます。
ターミナルでDebugger attachedになるとブレークポイントも灰色から赤くなります。

この状態でファイルをアップロードしてみます。workというコンテナーを先ほど作成したのでここにファイルアップロードしてみます。

ファイルがアップロードされ、vscodeに戻るとブレークポイントが発火しているのがわかります。

この状態でデバッグコンソールに引数で渡ってくるcontextやblobを入力してみます。
仮引数それぞれの変数にマウスカーソルを当てればツールチップで表示することもできます。

次に、別のhogeコンテナーにファイルをアップロードしてみます。

今回はブレークポイントが発火しませんでした。

というのも、下記のpathにworkコンテナーで絞る設定が入っているからでした。

app.storageBlob('storageBlobTrigger', {
  path: 'work/{name}',
  connection: '',
  handler: storageBlobTrigger,
});

Pathについて

公式の内容はこちらが参考になります。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-storage-blob-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&pivots=programming-language-javascript#configuration

まず、ここではpathに何が設定できるかを確認します。

公式にも書いてますが、Blobパターン名というので指定できます。

さて、先ほどhogeコンテナーにファイルをアップロードしてトリガーが発火しませんでした。
コンテナーを横断してトリガーを発火させることができるかを調べてみました。

調べた限りでは、コンテナー (ポーリング)のトリガーではできないとなります。

もちろん、複数のエンドポイントを作成してhoge用の関数を作成することで対応することはできます。

他にもEvent Gridを使えば可能ということはわかりました。

コンテナーを横断して発火させる設定は現状無理というのがわかったので他を確認していきます。

拡張子で絞る

以下のように拡張子を指定してフィルターをかけることもできます。

app.storageBlob('storageBlobTrigger', {
  path: 'work/{name}.test',
  connection: '',
  handler: storageBlobTrigger,
});

上記の場合、先ほどのファイルsample.txtではトリガーが発火しません。

BLOB トリガーのバインディング

こちらも合わせて参照してください。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-expressions-patterns#trigger-file-name

下記のように{name}としていたところを{hoge}に変えてみます。

app.storageBlob('storageBlobTrigger', {
  path: 'work/{hoge}',
  connection: '',
  handler: storageBlobTrigger,
});

この{hoge}は下記のように、context.triggerMetadataのkeyに出現します。
ここは先ほどnameだったのでこのキー情報が上記で定義したものとなるようです。

connectionについて

ここで個人的に一番躓きポイントは、connection stringを設定する項目ではないというところです。

例えば、以下のようにconnection stringを設定してみます。

app.storageBlob('storageBlobTrigger', {
  path: 'work/{name}.test',
  connection:
    'AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;',
  handler: storageBlobTrigger,
});

しかし、このままだとAzure Functionsの起動時にエラーが出ます。

エラー内容詳細
Azure Functions Core Tools
Core Tools Version:       4.0.6280 Commit hash: N/A +421f0144b42047aa289ce691dc6db4fc8b6143e6 (64-bit)
Function Runtime Version: 4.834.3.22875

[2024-09-28T05:00:15.638Z] Debugger attached.
[2024-09-28T05:00:15.795Z] Worker process started and initialized.
[2024-09-28T05:00:15.961Z] Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.storageBlobTrigger'. Microsoft.Azure.WebJobs.Extensions.Storage.Blobs: Storage account connection string 'AzureWebJobsAccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;' does not exist. Make sure that it is a defined App Setting.
[2024-09-28T05:00:16.004Z] Error indexing method 'Functions.storageBlobTrigger'
[2024-09-28T05:00:16.004Z] Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.storageBlobTrigger'. Microsoft.Azure.WebJobs.Extensions.Storage.Blobs: Storage account connection string 'AzureWebJobsAccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;' does not exist. Make sure that it is a defined App Setting.
[2024-09-28T05:00:16.005Z] Function 'Functions.storageBlobTrigger' failed indexing and will be disabled.
[2024-09-28T05:00:16.009Z] No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
[2024-09-28T05:00:16.074Z] The 'storageBlobTrigger' function is in error: Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.storageBlobTrigger'. Microsoft.Azure.WebJobs.Extensions.Storage.Blobs: Storage account connection string 'AzureWebJobsAccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;' does not exist. Make sure that it is a defined App Setting.

Functions:

        storageBlobTrigger: blobTrigger

For detailed output, run func with --verbose flag.
[2024-09-28T05:00:20.734Z] Host lock lease acquired by instance ID '000000000000000000000000F5594AAA'.

では実際にどうやってconnectionを使うかというとドキュメントにちゃんと書かれています。

ただ、ここに書かれている

接続文字列を含むアプリケーション設定の名前

というのが少しわかりにくいので、これが何かを説明します。

結論から言うと、local.setting.jsonに設定したキーをここに設定します。(Functionsに登録している場合はアプリケーション設定になります。)

以下のようにlocal.setting.jsonにBlobの接続文字列を設定したRemoteStorageというのを追加しました。

app.storageBlobも修正しました。

app.storageBlob('storageBlobTrigger', {
  path: '0928/{name}',
  connection: 'RemoteStorage',
  handler: storageBlobTrigger,
});

上記状態でRemoteStorageに設定したBlob(0928コンテナ)にファイルをアップロードしてみます。

下記のようにブレークポイントが発火しました。

ところで、当初このconnectionは空文字にしていました。この場合は下記からも分かるようにAzureWebJobsStorageが規定値として使用されます。

connection を空のままにした場合、Functions ランタイムは、アプリ設定内の AzureWebJobsStorage という名前の既定のストレージ接続文字列を使用します。

ID ベースの接続

こちらは下記のような設定で動作確認しました。

こちらも参考にしてください。

先ほどと変わっていない。

app.storageBlob('storageBlobTrigger', {
  path: '0928/{name}',
  connection: 'RemoteStorage',
  handler: storageBlobTrigger,
});

local.setting.json
connectionに入れたRemoteStorageというのが<CONNECTION_NAME_PREFIX>__blobServiceUriの<CONNECTION_NAME_PREFIX>にあたります。なので以下のように設定します。

RemoteStorage__blobServiceUri

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "AzureWebJobsStorage": "AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://my_azurite:10000/devstoreaccount1;QueueEndpoint=http://my_azurite:10001/devstoreaccount1;TableEndpoint=http://my_azurite:10002/devstoreaccount1;",
    "RemoteStorage__blobServiceUri": "https://<storage_account_name>.blob.core.windows.net",
    "RemoteStorage__queueServiceUri": "https://<storage_account_name>.queue.core.windows.net"
  }
}

次に、ターミナルでaz loginします。

これでDefault Credentialがaz loginした情報となります。

しかし、これだけだとエラーが出ます。

不思議です。というのも、az loginしたユーザはOwnerだったので。

調べてみるとこちらにも書いてますが、Ownerでは不十分なようです。

実行時に BLOB コンテナーへのアクセスを提供するロールの割り当てを作成する必要があります。 所有者のような管理ロールでは十分ではありません。

これはちょっとハマりポイントかなと思います。
Storage Blob Data Contributorに追加します。

また、この設定を入れても即時反映されないのか、しばらく待つ必要があります。なので、ちゃんと設定項目がわかっていないとハマります。設定して一旦10分くらい休憩して確認するという感じが良いでしょう。
こういう即時反映されない系って非常に混乱を招きます。

Appendix

devcontainerの準備

過去にも似たような記事を書いていることが判明しました。。。
なのでApendixにしました。

基本的にDevcontainerで作業します。ここでAzuriteのコンテナーも用意することになります。

docker-compose.yml、devcontainer.json、Dockerfileは、Visual Studio CodeのDev Containers機能を使用して、一貫性のある開発環境を構築するために用意されています。主な目的は以下の通りです:

  • 一貫した開発環境の提供: チーム全体で同じ開発環境を使用することができ、「自分の環境では動作するが、他の人の環境では動作しない」といった問題を回避できます。
  • 簡単なセットアップ: 新しいメンバーがプロジェクトに参加した際、複雑な環境設定を行う必要がなく、すぐに開発を始めることができます。
  • Azure Functionsの開発環境: Node.jsとAzure Functions Core Toolsが事前にインストールされた環境を提供し、Azure Functionsの開発をすぐに始められるようにしています。
  • Azuriteエミュレーターの統合: Azure Storageのエミュレーターを含めることで、ローカルでAzure Storageを使用する開発とテストが可能になります。
  • VS Code拡張機能の自動インストール: 開発に必要な拡張機能(Azure Functions、ESLint、Jestなど)を自動的にインストールし、開発体験を向上させています。

これらの設定により、開発者はプロジェクトのクローンを作成し、VS CodeでDev Containerを開くだけで、すぐにAzure Functionsの開発を始めることができる環境が整います。

コンテナーで再度開くとすれば、コンテナーの中で作業するような感じになります。

docker-compose.yml:

このdocker-compose.ymlファイルは、Azure Functions開発環境とAzuriteエミュレーターを含む開発環境を設定しています。主な内容は以下の通りです:

  • app サービス: Azure Functions アプリケーションを実行するためのコンテナ。Dockerfileを使用してビルドされ、ホストのワークスペースをマウントしています。
  • azurite サービス: Azure StorageエミュレーターであるAzuriteを実行するコンテナ。ポート10000、10001、10002をホストにマッピングしています。
  • ネットワーク設定: 両サービスは’my_sample_network’という名前のブリッジネットワークに接続されています。

この設定により、ローカル開発環境でAzure FunctionsとAzure Storageの機能をAzuriteでエミュレートすることができます。

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    platform: linux/amd64
    networks:
      - my_sample_network
    container_name: my_app
    volumes:
      - ../..:/workspaces:cached
    command: sleep infinity

  azurite:
    image: mcr.microsoft.com/azure-storage/azurite
    restart: unless-stopped
    networks:
      - my_sample_network
    container_name: my_azurite
    ports:
      - 127.0.0.1:10000:10000
      - 127.0.0.1:10001:10001
      - 127.0.0.1:10002:10002

networks:
  my_sample_network:
    driver: bridge

devcontainer.json

devcontainer.jsonは、VS Codeで開発環境をコンテナ上に構築するための設定ファイルです。Azure CLIAzure Functions Core Toolsなどのツールがインストールされ、ポート7071などがフォワードされます。VS Codeの拡張機能や設定も含まれており、PrettierでのコードフォーマットやJestでのテストが簡単に実行可能です。postCreateCommandで依存パッケージも自動インストールされます。

{
  "name": "Azure Functions (Node.js)",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspaces",
  "forwardPorts": [7071, 10000, 10001, 10002],
  "otherPortsAttributes": {
    "onAutoForward": "ignore"
  },
  "features": {
    "ghcr.io/devcontainers/features/azure-cli:1": {},
    "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {}
  },
  "customizations": {
    "vscode": {
      "settings": {
        "extensions.verifySignature": false,
        "jest.runMode": "deferred"
      },
      "extensions": [
        "ms-azuretools.vscode-azurefunctions",
        "dbaeumer.vscode-eslint",
        "firsttris.vscode-jest-runner",
        "Orta.vscode-jest",
        "GitHub.copilot",
        "esbenp.prettier-vscode",
        "VisualStudioExptTeam.vscodeintellicode",
        "oderwat.indent-rainbow"
      ]
    },
    "settings": {
      "files.encoding": "utf8",
      "files.eol": "\n",
      "editor.defaultFormatter": "esbenp.prettier-vscode",
      "workbench.iconTheme": "material-icon-theme",
      "": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      }
    }
  },
  "postCreateCommand": "npm install"
}

Dockerfile

FROM mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
bookwormはDebianの最新安定版ディストリビューションで、このイメージにはNode.js 20がインストールされています。この設定により、Node.jsを使用するJavaScriptの開発環境がDebianベースのコンテナ上で提供されます。

デバッグ構成

以下を設定することでvscodeでデバッグできるようになります。

/.vscode/launch.json

基本的にはDebug Azure Functionのデバッグ構成を使用します。
「Attach to Node Functions」について この構成では、すでに実行中のNode.js Azure Functionsにデバッガを接続するために使用されます。関数アプリがデバッグモードで起動している場合、port で指定されたポート (ここは使用しているポートによって変更してください) を通じて接続が可能です。ポート番号が他のプロセスと競合することがあるため、特にローカルで複数のアプリケーションが動作している場合は注意が必要です。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Azure Function",
      "request": "launch",
      "runtimeArgs": ["run-script", "start"],
      "runtimeExecutable": "npm",
      "skipFiles": ["<node_internals>/**"],
      "type": "node",
      "console": "integratedTerminal"
    },
    {
      "name": "Attach to Node Functions",
      "type": "node",
      "request": "attach",
      "port": 7071,
      "preLaunchTask": "func: host start"
    }
  ]
}