Azure Functionsをローカル実行してマネージドIDを使った認証でBlobStorageにアクセスしたメモ

## 概要
Azure Blob Storageのデータを取得するAPIをローカルで動かした。
認証にはマネージドIDを使ってみたメモ。

[ソースコード](https://github.com/hibohiboo/Yakumi/blob/2b74f95a5a4f766be8bbf7a47c3c80b75e342843/api/src/functions/magiaCharacter.ts)

## コード

```ts:api/src/lib/azure-storage-with-credential.ts
import { DefaultAzureCredential } from '@azure/identity';
import { BlobServiceClient } from '@azure/storage-blob';

export const getBlobClient = () => {
  const accountName = process.env.Azure_Storage_AccountName;
  if (!accountName) throw Error('Azure Storage accountName not found');

  const blobServiceClient = new BlobServiceClient(
    `https://${accountName}.blob.core.windows.net`,
    new DefaultAzureCredential(),
  );
  return blobServiceClient;
};

```

```ts:api/src/functions/magiaCharacter.ts
import {
  app,
  HttpRequest,
  HttpResponseInit,
  InvocationContext,
} from '@azure/functions';
import { getBlobClient } from '@yakumi-api/lib/azure-storage-with-credential';

export async function magiaCharacter(
  request: HttpRequest,
  context: InvocationContext,
): Promise<HttpResponseInit> {
  const { uid, id } = request.params;
  context.log(`uid: ${uid}, id: ${id}`);
  const bloblClient = getBlobClient();
  const container = bloblClient.getContainerClient('$web');
  const blob = container.getBlobClient(
    `fall-magia/character-data/${uid}/${id}/character-data.json`,
  );
  const res = await blob.download();
  if (res.errorCode || !res.readableStreamBody) {
    throw new Error('Blob not found');
  }
  const data = JSON.parse(
    (await streamToBuffer(res.readableStreamBody)).toString(),
  );
  return {
    status: 200,
    headers: new Headers([['Content-Type', 'application/json']]),
    body: JSON.stringify(data),
  };
}

app.http('magiaCharacter', {
  methods: ['GET'],
  authLevel: 'anonymous',
  route: 'magia-character/{uid}/{id}',
  handler: magiaCharacter,
});

function streamToBuffer(
  readableStreamBody: NodeJS.ReadableStream,
): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    readableStreamBody.on('data', (chunk) => {
      const content = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
      chunks.push(content);
    });
    readableStreamBody.on('end', () => {
      const buffer = Buffer.concat(chunks);
      resolve(buffer);
    });
    readableStreamBody.on('error', reject);
  });
}
```

## マネージドIDの設定
リソースグループのアクセス制御(IAM)から設定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/09e272da-b835-ff5b-6f09-42162af2fb2b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/f5ba8839-7fe5-0afa-5d23-56b37f9110ee.png)

BloBストレージのCRUD操作ができるロールを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/4687ce99-fead-f612-75c7-d90e1bf5a863.png)

メンバーから自分を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/e0e877f1-f601-b75c-eb40-0e3296513d31.png)

選択したメンバーになったことを確認して、選択ボタンを押下

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/ff6bda4b-04b5-490a-e082-b1654ae3f825.png)


条件は特になし

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/2a60baa5-c690-ad75-d228-f63d6ba6a12d.png)

スコープがリソースグループ内になっていることを確認して、割り当て

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/246da043-f6e0-b2be-8ec1-f1bb1801ab6b.png)


## 確認

割り当て後、ローカルで`npm run start`でAPIを立ち上げ、アクセスしたところBlobコンテナ内のファイルを取得できた。

## メモ

マネージドIDのロールを割り当てる前は下記のエラーが発生。

```
 System.Private.CoreLib: Exception while executing function: Functions.magiaCharacter. System.Private.CoreLib: Result: Failure
Exception: This request is not authorized to perform this operation using this permission.
```

## 参考

[Azure SDK for JavaScript を使用して Azure リソースに対して Azure でホストされるアプリを認証する](https://learn.microsoft.com/ja-jp/azure/developer/javascript/sdk/authentication/azure-hosted-apps?tabs=azure-cli%2Cazure-app-service)
[TypeScript を使って BLOB をダウンロードする](https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-download-typescript)

## 2024.09.19 追記
GUIではなくbicepを使って設定する方法を追記する。

```:infra/biceps/rbac.bicep
@description('Specifies the role definition ID used in the role assignment.')
param roleDefinitionID string

@description('Specifies the principal ID assigned to the role.')
param principalId string

// リソースグループの単位でロールを割り当てる
var roleAssignmentName= guid(principalId, roleDefinitionID, resourceGroup().id)
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: roleAssignmentName
  properties: {
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID)
    principalId: principalId
  }
}
```

```bash:infra/bin/rbac.bash
#!/bin/bash

BIN_DIR=$(cd $(dirname $0) && pwd)
BICEP_DIR=$(cd $BIN_DIR/../biceps && pwd)

# 環境変数読み込み
source $BIN_DIR/.env

# ローカルでログイン中のユーザのIDを取得
PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv)

# 組み込みロール
# https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles

# ストレージ BLOB データ共同作成者 のロールを割り当てる
cd $BICEP_DIR && az deployment group create \
  --name frontendDeployment \
  --template-file rbac.bicep \
  --parameters roleDefinitionID=ba92f5b4-2d11-453d-a403-e96b0029c9fe \
  principalId=$PRINCIPAL_ID \
  -g $RESOURCE_GROUP_NAME

# ストレージ キュー データ共同作成者 のロールを割り当てる
cd $BICEP_DIR && az deployment group create \
  --name frontendDeployment \
  --template-file rbac.bicep \
  --parameters roleDefinitionID=974c5e8b-45b9-4653-ba55-5f855dd0fb88 \
  principalId=$PRINCIPAL_ID \
  -g $RESOURCE_GROUP_NAME

```

[参考: 組み込みロールのroleDefinitionID](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles)
[参考: クイックスタート: Bicep を使用して Azure でのロールを割り当てる](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/quickstart-role-assignments-bicep?tabs=CLI)

なお、ローカルから接続する場合はファイアウォールの設定で許可する必要がある。
下記はすべて開けた場合のbicep

```diff
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: '${storageAccountName}storage'
  location: location
  sku: { name: storageAccountSku }
  kind: 'StorageV2'
  properties: {
+    allowBlobPublicAccess: true
    minimumTlsVersion: 'TLS1_2'
+    publicNetworkAccess: 'Enabled'
  }
}
```

試す。
[ソースコード](https://github.com/hibohiboo/async-ttrpg/tree/58519da092840babc0828b570c0171cac2856a4a)

```
cd apps/api && npm run dev
```

```
post http://localhost:7071/api/characters/async
Content-Type: application/json

{
  "CharacterID": "test1",
  "CharacterName": "テストキャラクターあるふぁ"
}
```