0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-09-18

概要

前回はローカル環境で実行したFunctionsからBlobStorageに接続した。

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

ソースコード

bicep

Blobの書き込み、Queueの書き込みに必要なロールのIDはAzure 組み込みロールから探した

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
+   }
+ }
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'
  }
}

コード

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のエラーが発生

キューの組み込みロール「ストレージ キュー データ共同作成者」を割り振っていないときはAuthorizationPermissionMismatchのエラーが発生。

ローカル実行

azuriteを使ったローカル実行を試した。
https-setup
を参考にまずはazuriteのhttps化を行う

choco install mkcert
mkcert -install

下記の警告がでるが、そのまま証明書をインストールする。
image.png

cd hoge_dir
mkcert 127.0.0.1

127.0.0.1.pem127.0.0.1-key.pemが作成される。

azuriteのcertオプションに指定する。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.
    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で立ち上げていた接続を使おうとすると下記エラーとなる。

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

https用の接続設定が必要

image.png

テスト時にRestError: unable to verify the first certificate が出る件

    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を追加し、オレオレ証明書を許容する設定が必要であった。

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

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

ソースコード

参考

async-ttrpg - issue

sdk
クイックスタート: Node.js 用 Azure Blob Storage クライアント ライブラリ
クイックスタート: JavaScript 用 Azure Queue Storage クライアント ライブラリ

bicep
参考
storage account bicep

認証
Bicep を使用して Azure RBAC リソースを作成する
blob
queue
Bicep を使用して Azure カスタム ロールを作成または更新する
Azure 組み込みロール

世界一わかりみの深いマネージドID 〜ソースコードから資格情報を追い出そう!!〜
クイックスタート: Bicep を使用して Azure でのロールを割り当てる
storage account 403

JavaScript 用 Azure ID クライアント ライブラリ - バージョン 4.5.0
ローカルでの Azure Storage の開発に Azurite エミュレーターを使用する
ローカルでの Azure Storage の開発に Azurite エミュレーターを使用する
azurite-https-defaultazurecredentia

get-blob
list

Dドライブで実行したnpm scriptsで ~ を指定したらCドライブのホームディレクトリを見てくれなかったので書きかたを修正したメモ
issue

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?