概要
前回はローカル環境で実行したFunctionsからBlobStorageに接続した。
今回はFunctionsが持つシステムマネージドIDを有効化してBlobに接続する。
bicep
Blobの書き込み、Queueの書き込みに必要なロールのIDはAzure 組み込みロールから探した
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
+ }
+ }
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
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'
}
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
cd hoge_dir
mkcert 127.0.0.1
127.0.0.1.pem
と127.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用の接続設定が必要
テスト時に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
を追加し、オレオレ証明書を許容する設定が必要であった。
import settings from './local.settings.json';
// 省略
env: {
TZ: 'UTC',
AzureWebJobsStorage: settings.Values.AzureWebJobsStorage,
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
},
// 省略
参考
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
Dドライブで実行したnpm scriptsで ~ を指定したらCドライブのホームディレクトリを見てくれなかったので書きかたを修正したメモ
issue