Azure FunctionsのマネージドIDを有効化してBlobStorageにアクセスしたメモ

# 概要
[前回](https://qiita.com/hibohiboo/items/b1efc4f8431d742b0e0f)はローカル環境で実行したFunctionsからBlobStorageに接続した。

今回はFunctionsが持つシステムマネージドIDを有効化してBlobに接続する。

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

# bicep
Blobの書き込み、Queueの書き込みに必要なロールのIDは[Azure 組み込みロール](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles)から探した

```diff:infra/biceps/main.bicep
param location string = resourceGroup().location
param keyVaultName string
@description('The runtime version of the Azure Functions app.')
param functionsRuntime object
param functionEnvironments array 
param staticSites_pl_static_web_app_name string

var storageAccountName = '${uniqueString(resourceGroup().id)}azfunctions'
var applicationInsightsName = '${uniqueString(resourceGroup().id)}applicationinsights'
var logAnalyticsName = '${uniqueString(resourceGroup().id)}logAnalytics'
+ var queueAndContainerStorageAccountName = '${uniqueString(resourceGroup().id)}azstorage'

resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: keyVaultName
}

module myFunctionsApplicationInsights 'core/host/applications.bicep' = {
  name: 'myFunctionsApplicationInsights'
  params: {
    location: location
    applicationInsightsName: applicationInsightsName
    logAnalyticsName: logAnalyticsName
  }
}

module myFunctionsStorage 'core/storage/storage-account.bicep' = {
  name: 'myFunctionsStorage'
  params: {
    location: location
    storageAccountName: storageAccountName
  }
}

module myFunctions 'core/host/functions.bicep' = {
  name: 'myFunctions'
  params: {
    location: location
    staticSites_pl_static_web_app_name: staticSites_pl_static_web_app_name
    databaseUrl: keyVault.getSecret('AsyncTrpgDatabaseURL')
    storageAccountName: storageAccountName
    kind: functionsRuntime.kind
    runtime: functionsRuntime.runtime
    linuxFxVersion: functionsRuntime.linuxFxVersion
    applicationInsightsInstrumentationKey: myFunctionsApplicationInsights.outputs.applicationInsightsInstrumentationKey
    extensionVersion: functionsRuntime.extensionVersion
    connectionString: keyVault.getSecret('AsyncTrpgConnectionString')
    functionEnvironments: functionEnvironments
+     queueAndContainerStorageAccountName:queueAndContainerStorageAccountName
  }
}

output appServiceAppHostName string = myFunctions.outputs.appServiceAppHostName

+ module myStorageBlobContainerAndQueue 'core/storage/blobContainerAndQueue.bicep' = {
+   name: 'myStorageBlobContainerAndQueue'
+   params: {
+     location: location
+     storageAccountName: queueAndContainerStorageAccountName
+   }
+ }
+ // 組み込みロール: https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles
+ var storageRoleDefinitionId= 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // ストレージ BLOB データ共同作成者
+ var queueRoleDefinitionId= '974c5e8b-45b9-4653-ba55-5f855dd0fb88' // ストレージ キュー データ共同作成者
+ var principalId = myFunctions.outputs.principalId
+ module myStorageRole 'core/rbac/role.bicep' = {
+   name: 'myStorageRole'
+   params: {
+     queueAndContainerStorageAccountName: queueAndContainerStorageAccountName
+     principalId: principalId
+     roleDefinitionId: storageRoleDefinitionId
+   }
+ }
+ module myQueueRole 'core/rbac/role.bicep' = {
+   name: 'myQueueRole'
+   params: {
+     queueAndContainerStorageAccountName: queueAndContainerStorageAccountName
+     principalId: principalId
+     roleDefinitionId: queueRoleDefinitionId
+   }
+ }
```
```diff:infra/biceps/core/host/functions.bicep
param storageAccountName string
param location string
param runtime string
param kind string
param linuxFxVersion string
param extensionVersion string
param applicationInsightsInstrumentationKey string
@secure()
param databaseUrl string
@secure()
param connectionString string
param functionEnvironments array 
param staticSites_pl_static_web_app_name string
+ param queueAndContainerStorageAccountName string

var functionAppName = '${uniqueString(resourceGroup().id)}azfunctionsapp'
var environments = [
  {
    name: 'DATABASE_URL'
    value: databaseUrl
  }
  {
    name:'CONNECTION_STRING'
    value: connectionString
  }
+  {
+    name: 'BLOB_QUEUE_STORAGE_ACCOUNT_NAME'
+    value: queueAndContainerStorageAccountName
+  }
  ...functionEnvironments
]
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
  name: storageAccountName
}
resource staticSites_my_first_static_web_app_name_resource 'Microsoft.Web/staticSites@2023-01-01' existing = {
  name: staticSites_pl_static_web_app_name
}

resource functionApp 'Microsoft.Web/sites@2022-09-01' = {
  name: functionAppName
  location: location
  kind: kind
+  identity: {
+    type: 'SystemAssigned'
+  }
  properties: {
    reserved: true
    siteConfig: {
      cors: {
        allowedOrigins: [staticSites_my_first_static_web_app_name_resource.properties.defaultHostname]
      }
      linuxFxVersion: linuxFxVersion
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: toLower(functionAppName)
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: extensionVersion
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: runtime
        }
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsightsInstrumentationKey
        }
        ...environments
      ]
    }
  }
}

output appServiceAppHostName string = functionApp.properties.defaultHostName
+ output principalId string = functionApp.identity.principalId
```

```:infra/biceps/core/storage/blobContainerAndQueue.bicep
param location string
param storageAccountName string
var storageAccountSku = 'Standard_LRS'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: { name: storageAccountSku }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
  }
}
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  parent: storageAccount
  name: 'default'
}
resource blobContainerForQueue 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
  parent: blobService
  name: 'character-container'
}
resource queueServices 'Microsoft.Storage/storageAccounts/queueServices@2023-01-01' = {
  parent: storageAccount
  name: 'default'
}
resource storageQueueMain 'Microsoft.Storage/storageAccounts/queueServices/queues@2023-01-01' = {
  parent: queueServices
  name: 'character-queue'
}
```

```:infra/biceps/core/rbac/role.bicep
param queueAndContainerStorageAccountName string
param principalId string
param roleDefinitionId string

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
  name: queueAndContainerStorageAccountName
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: storageAccount
  name: guid(storageAccount.id, principalId, roleDefinitionId)
  properties: {
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
    principalId: principalId
    principalType: 'ServicePrincipal'
  }
}
```

# コード

```ts
import { prisma } from '../shared/prisma';
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { CharacterSchema } from '@db/zod';
import { z } from 'zod';
import { AppContext } from '@api/types';
import { BlobServiceClient } from '@azure/storage-blob';
import { DefaultAzureCredential } from '@azure/identity';
import { QueueServiceClient } from '@azure/storage-queue';
import { sendQueueAndBlobContainer } from '@api/lib/sendQueueAndBlobContainer';

const app = new Hono<AppContext>()
  .post('/async', zValidator('json', CharacterSchema), async (c) => {
    const logger = c.env.AZURE_FUNCTIONS_CONTEXT;
    const data = await c.req.valid('json');
    // eslint-disable-next-line turbo/no-undeclared-env-vars
    const accountName = process.env.BLOB_QUEUE_STORAGE_ACCOUNT_NAME;
    if (!accountName) {
      throw new Error('Missing BLOB_QUEUE_STORAGE_ACCOUNT_NAME env var');
    }
    const credential = new DefaultAzureCredential();
    const blobServiceClient = new BlobServiceClient(
      `https://${accountName}.blob.core.windows.net`,
      credential,
    );
    const containerClient = blobServiceClient.getContainerClient(
      'character-container',
    );

    const queueServiceClient = new QueueServiceClient(
      `https://${accountName}.queue.core.windows.net`,
      credential,
    );
    const queueClient = queueServiceClient.getQueueClient('character-queue');

    await sendQueueAndBlobContainer({
      containerClient,
      queueClient,
      blobPath: `${data.CharacterID}.json`,
      blobData: JSON.stringify(data),
      messageTimeToLive: 60 * 60 * 0.5, // 30 minutes
      logger,
    });
    return c.json({});
  });

export default app;
```


# 試す

```
post  https://hoge.azurewebsites.net/api/characters/async
Content-Type: application/json

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

# エラー
パブリックネットワークのアクセスを許可しなかったときは`AuthorizationFailure`のエラーが発生

キューの組み込みロール「[ストレージ キュー データ共同作成者](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles/storage#storage-queue-data-contributor)」を割り振っていないときは`AuthorizationPermissionMismatch`のエラーが発生。

# ローカル実行
azuriteを使ったローカル実行を試した。
[https-setup](https://github.com/Azure/Azurite/blob/main/README.md#https-setup)
を参考にまずはazuriteのhttps化を行う

```
choco install mkcert
mkcert -install
```

下記の警告がでるが、そのまま証明書をインストールする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/df066606-331f-627b-b756-9bafbc9d2940.png)

```
cd hoge_dir
mkcert 127.0.0.1
```

`127.0.0.1.pem`と`127.0.0.1-key.pem`が作成される。

[azuriteのcertオプションに指定](https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=visual-studio%2Cblob-storage#certificate-configuration-https)する。oauthオプションも有効にする

```
 azurite --silent --location c:\azurite --debug c:\azurite\debug.log  --oauth basic --cert ~/.ssh/mkcert/127.0.0.1.pem --key ~/.ssh/mkcert/127.0.0.1-key.pem
 ```
 

## ローカル実行の失敗例
### httpで接続しようとする

```
Error: Bearer token authentication is not permitted for non-TLS protected (non-https) URLs.
```

```diff
    const credential = new DefaultAzureCredential();
    const blobServiceClient = new BlobServiceClient(
-      `http://127.0.0.1:10000/devstoreaccount1`,
      credential,
    );
```

```
azurite --silent --location c:\azurite --debug c:\azurite\debug.log  --oauth basic
```


### Azure Storage Explorer での接続に失敗する

httpsで立ち上げて、httpで立ち上げていた接続を使おうとすると下記エラーとなる。

```json
ProducerError:{
  "name": "Electron Net Error",
  "message": "{\"name\":\"Electron Net Error\",\"cause\":{}}"
}
```

https用の接続設定が必要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/d2780f72-607c-514a-f83e-3fd70db05fe6.png)


### テスト時にRestError: unable to verify the first certificate が出る件
```ts
    const credential = new DefaultAzureCredential();
    const blobServiceClient = new BlobServiceClient(
      `https://127.0.0.1:10000/devstoreaccount1`,
      credential,
    );
```

```
: RestError: unable to verify the first certificate
UNABLE_TO_VERIFY_LEAF_SIGNATURE
```

環境変数に`NODE_TLS_REJECT_UNAUTHORIZED`を追加し、オレオレ証明書を許容する設定が必要であった。

```diff:vitest.config.ts:
import settings from './local.settings.json';

// 省略
  env: {
    TZ: 'UTC',
    AzureWebJobsStorage: settings.Values.AzureWebJobsStorage,
+    NODE_TLS_REJECT_UNAUTHORIZED: '0',
  },
// 省略
```

[ソースコード](https://github.com/hibohiboo/async-ttrpg/tree/develop/apps/api)

# 参考

[async-ttrpg - issue](https://github.com/hibohiboo/async-ttrpg/issues/31)

sdk
[クイックスタート: Node.js 用 Azure Blob Storage クライアント ライブラリ](https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-quickstart-blobs-nodejs?tabs=managed-identity%2Croles-azure-portal%2Csign-in-azure-cli&pivots=blob-storage-quickstart-scratch)
[クイックスタート: JavaScript 用 Azure Queue Storage クライアント ライブラリ](https://learn.microsoft.com/ja-jp/azure/storage/queues/storage-quickstart-queues-nodejs?tabs=passwordless%2Croles-azure-portal%2Cenvironment-variable-windows%2Csign-in-azure-cli)

bicep
[参考](https://learn.microsoft.com/en-us/answers/questions/1019546/deployment-of-queue-blob-and-adls2-private-endpoin)
[storage account bicep](https://learn.microsoft.com/ja-jp/azure/templates/microsoft.storage/storageaccounts/blobservices?pivots=deployment-language-bicep)

 認証
[Bicep を使用して Azure RBAC リソースを作成する](https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/scenarios-rbac)
[blob](https://learn.microsoft.com/ja-jp/azure/developer/javascript/sdk/authentication/azure-hosted-apps?tabs=azure-cli%2Cazure-app-service)
[queue](https://learn.microsoft.com/ja-jp/javascript/api/overview/azure/storage-queue-readme?view=azure-node-latest)
[Bicep を使用して Azure カスタム ロールを作成または更新する](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/custom-roles-bicep?tabs=CLI)
[Azure 組み込みロール](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles)

[世界一わかりみの深いマネージドID 〜ソースコードから資格情報を追い出そう！！〜](https://tech-lab.sios.jp/archives/27461)
[クイックスタート: Bicep を使用して Azure でのロールを割り当てる](https://learn.microsoft.com/ja-jp/azure/role-based-access-control/quickstart-role-assignments-bicep?tabs=CLI)
[storage account 403](https://jpaztech.github.io/blog/storage/storageFirewall-403Error/)

[JavaScript 用 Azure ID クライアント ライブラリ - バージョン 4.5.0](https://learn.microsoft.com/ja-jp/javascript/api/overview/azure/identity-readme?view=azure-node-latest#authenticate-via-development-tools)
[ローカルでの Azure Storage の開発に Azurite エミュレーターを使用する](https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage) 
[ローカルでの Azure Storage の開発に Azurite エミュレーターを使用する](https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage) 
[azurite-https-defaultazurecredentia](https://blog.jongallant.com/2020/02/azurite-https-defaultazurecredential/#google_vignette)

[get-blob](https://learn.microsoft.com/ja-jp/rest/api/storageservices/get-blob?tabs=microsoft-entra-id#emulated-storage-service-uri)
[list](https://learn.microsoft.com/ja-jp/rest/api/storageservices/list-containers2?tabs=microsoft-entra-id#emulated-storage-service-request)

[Dドライブで実行したnpm scriptsで ~ を指定したらCドライブのホームディレクトリを見てくれなかったので書きかたを修正したメモ](https://qiita.com/hibohiboo/items/4494e8771931ea209d4f)
[issue](https://github.com/hibohiboo/async-ttrpg/issues/166)