AzureFunction経由でBlobにログを書き込む（AppendBlob）

# はじめに

ログをクラウド上に持ってくるときにWebHookを経由して連携させるSaaSのソリューションがあります。

こんなイメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/7af9b01e-9fab-4fd9-9712-ecb32f06a285.png)


SendGridのアクティビティログはまさにそのパターンで、例えばEvent Webhookを利用することでSendGrid経由でメールを送信する際に発生するイベントを、指定したURLにPOSTすることができます。

```
このデータの用途は、配信停止アドレスの削除、迷惑メール報告への対応、
エンゲージできなかった受信アドレスの判定、バウンスされたメールアドレスの特定、
メールプログラムの高度な分析などです。ユニーク引数やカテゴリパラメータを使用して、
動的なデータを挿入することができるため、あなたのメールのシャープでクリアなイメージを構築するのに役立ちます。
```

https://sendgrid.kke.co.jp/docs/API_Reference/Webhooks/event.html

これをAzure Functionsを利用して実装してみたいと思います。

＃AzureFunction(Typescript)でAppendBlobを利用する記事が見当たらなかったので書いてみました。

# output binding

Azure FunctionsにはOutput Bindingという機能があり、BlobStorageへ簡単にデータを書き込むことができます。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-storage-blob?tabs=in-process%2Cextensionv5%2Cextensionv3&pivots=programming-language-javascript

これを使えば簡単にできるぞと思っていざやってみるとそうはうまくいきませんでした。
常に新しいファイルに書き込んでいくことはできるのですが追記することはできないようです。

# Append blobの利用

こういうログのユースケースに最適なBlobStorageがありまして、これが追加Blob(append blob)です。

```text
追加 BLOB はブロックで構成され、追加操作用に最適化されています。 
追加 BLOB を変更すると、ブロックは追加ブロック操作を介してのみ BLOB の末尾に 追加 されます。
既存のブロックの更新または削除はサポートされていません。 
ブロック BLOB とは異なり、追加 BLOB はブロック ID を公開しません。

追加 BLOB 内の各ブロックは、最大 4 MiB の異なるサイズにすることができ、
追加 BLOB には最大 50,000 ブロックを含めることができます。
したがって、追加 BLOB の最大サイズは 195 GiB (4 MiB X 50,000 ブロック) をわずかに超えています。
```

一度の書き込み4MBというところは気を付けたほうがよさそうですが、追加処理に最適という部分がポイントです。

https://learn.microsoft.com/ja-jp/rest/api/storageservices/understanding-block-blobs--append-blobs--and-page-blobs


いざ、TypeScriptでoutputbindingを使ってAppendBlobを利用しようと思ったのですが、どうやらC#しか対応していない模様です。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-storage-blob-input?tabs=in-process%2Cextensionv5&pivots=programming-language-csharp

ということで、Function内でBlobSDKを使って書き込んでいきたいと思います。

# Azure Function

ソースはこちら

https://github.com/uzresk/sendgrid-tools


```typescript:index.ts
import {AzureFunction, Context, HttpRequest} from "@azure/functions"
import {BlobServiceClient, StorageSharedKeyCredential} from "@azure/storage-blob"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {

    // log
    context.log(req.body)

    const account = process.env.STORAGE_ACCOUNT_NAME || "";
    const accountKey = process.env.STORAGE_ACCOUNT_KEY || "";
    const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey);
    const blobServiceClient = new BlobServiceClient(
        `https://${account}.blob.core.windows.net`,
        sharedKeyCredential
    );
    // コンテナがなければ作る
    const containerClient = await blobServiceClient.getContainerClient("sendgrid");
    await containerClient.createIfNotExists();

    // append blobが無ければ作る
    const date = new Date().getDate();

    const appendBlobClient = await containerClient.getAppendBlobClient(`activity-${getYYYYMMDD(new Date())}.log`);
    await appendBlobClient.createIfNotExists();

    // append blobに追記
    let activity = JSON.stringify(req.body) + '\n';
    await appendBlobClient.getAppendBlobClient().appendBlock(activity, activity.length);

    context.res = {
        status: 200
    };

};

const getYYYYMMDD = (now: Date) => {
    let yyyy = now.getFullYear();
    let mm = ('00' + (now.getMonth() + 1)).slice(-2);
    let dd = ('00' + now.getDate()).slice(-2);
    const ymd = String(yyyy) + String(mm) + String(dd);
    return ymd;
};

export default httpTrigger;
```

## ローカル実行

1. Storageアカウントを<BlockBlob>作成
    - ストレージアカウントの名前とアクセスキーはメモしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/ef8f9280-e01c-3d66-39a6-3e8db9545d94.png)

2. Azure Functionを作成

3. STORAGE_ACCOUNT_NAME,STORAGE_ACCOUNT_KEYをFunctionの環境変数に追加
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/7e4920fb-d5a8-d985-b726-01b6f9fad9d0.png)

4. Functionの環境変数をローカルへダウンロード
  - local.settings.jsonに環境変数が入ることを確認する
  ```
  func azure functionapp fetch-app-settings <functionname>
  ```

5.ローカルで実行

  - 正常に起動することを確認します。
  ```
  func start
  ```

6.PostmanなどでJSONを投げ込んで、ストレージアカウントにデータが入ることを確認してください。

## deploy

```
yarn run build
```

```
func azure functionapp publish <functionname>
```

# SendGridでの利用

Functionでアプリキーをコピーします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57686/32418fb7-4e94-39aa-a450-3d587cfd197b.png)

`http://function-url/eventwebhook?code=<アプリキー>`をsendgridに設定します。

その他注意点についてはこちらの記事を参照してください。

https://qiita.com/uzresk/items/33d0c57d00df0d10ab6b