🍣

Azure FunctionsのBlobトリガーを使ってみる:Key Vaultを使った安全な接続

2024/08/03に公開

構成図

Azure FunctionsのBlobトリガーを使ってみました。

Blobの接続文字列の管理にはKeyVaultのシークレットを使用して、セキュアに実行できるようにしてみます。

ローカル環境でFunctionsを実行する

前提

各種ツールは既にインストール済みの前提で進みます。

  • .NET: 7.0.404
  • Azure CLI: 2.54.0
  • Azure Functions Core Tools: 4.0.5455
  • Azure Storage Explorer

事前準備

Azuriteのインストール

Azuriteは、BlobストレージをはじめとするAzure Storageサービスをローカル環境でテストするためのツールです。

https://learn.microsoft.com/ja-jp/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage

ローカル環境での検証に使用しますので、インストールします。

$ npm install -g azurite
$ azurite -v
3.29.0

ローカル環境で関数を実行する

関数アプリを作成する

下記のコマンドで関数アプリ用のプロジェクトを作成します。

# Functionsプロジェクトの基本的なファイルを作成します
$ func init --worker-runtime dotnet
$ dotnet build

# 関数ファイルを作成します
$ func new --name ProcessFile --template BlobTrigger

上記コマンドを実行すると、次のような構成のディレクトリやファイルが作成されます。

--nameで指定した名前で関数ファイルが作成されています(ProcessFile.cs)。

関数ファイルの編集

作成されたProcessFile.csを次のように編集します。

ProcessFile.cs
using System.IO;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;

namespace func_app
{
    public class ProcessFile
    {
        [FunctionName("ProcessFile")]
        public async Task Run(
            [BlobTrigger("input/{name}", Connection = "TestProjectStorage")] Stream inputBlob,
            string name,
            [Blob("output/{name}", FileAccess.Write, Connection = "TestProjectStorage")] Stream outputBlob,
            ILogger log)
        {
            log.LogInformation($"Blobファイルの処理を開始します\n Name:{name} \n Size: {inputBlob.Length} Bytes");
            await inputBlob.CopyToAsync(outputBlob);
        }
    }
}

今回はFunctionsのBlobトリガーの動作確認が目的です。

そのため、関数の内容自体はBlobストレージのinputコンテナ(ディレクトリ)にアップロードされた画像をoutputコンテナにコピーするだけの簡単な内容です。

設定ファイルの編集

local.settings.json
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "TestProjectStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet"
    }
}
  • AzureWebJobsStorage: 関数アプリと関連付けするストレージアカウントの接続文字列
  • TestProjectStorage: 関数アプリ内で使用するストレージアカウントの接続文字列
【補足】AzureWebJobsStorageとは

前提として、Functionsはストレージアカウントと関連付けされている必要があります。Functionsの操作にはストレージアカウントが必須であり、それがないとFunctionsは実行されないからです。

具体的には、トリガーの管理や関数の実行のログ記録などの用途で関連付けされたストレージアカウントが使用されます。

https://learn.microsoft.com/ja-jp/azure/azure-functions/storage-considerations?tabs=azure-cli#storage-account-guidance

AzureWebJobsStorageには、このFunctionsに関連付けされたストレージアカウントの接続文字列が指定されます。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobsstorage

ローカル環境で関数を実行する

まず、azuriteを起動しておきます。

$ azurite
Azurite Blob service is starting at http://127.0.0.1:10000
Azurite Blob service is successfully listening at http://127.0.0.1:10000
Azurite Queue service is starting at http://127.0.0.1:10001
Azurite Queue service is successfully listening at http://127.0.0.1:10001
Azurite Table service is starting at http://127.0.0.1:10002
Azurite Table service is successfully listening at http://127.0.0.1:10002

続いて、関数を実行します。

func startコマンドで関数を起動しておきます。

$ func start
....
Functions:
        ProcessFile: blobTrigger
For detailed output, run func with --verbose flag.

関数が起動した状態で、対象コンテナ(input)に画像(SampleImage.png)をアップロードします。

画像アップロードはAzure Storage Explorerから行います。

画像をアップロードすると、BlobTriggerが発火しました。

関数が実行された旨のログも出力されています。

[2024-01-20T21:11:16.570Z] Executing 'ProcessFile' (Reason='New blob detected(LogsAndContainerScan): input/SampleImage.png', Id=2e23e99d-1c40-4091-87e4-9e6407e5d57d)
[2024-01-20T21:11:16.570Z] Trigger Details: MessageId: 6f2759a0-4281-4e2f-ae42-f71bddd4cf4d, DequeueCount: 1, InsertedOn: 2024-01-20T21:11:16.000+00:00, BlobCreated: 2024-01-20T21:11:16.000+00:00, BlobLastModified: 2024-01-20T21:11:16.000+00:00
[2024-01-20T21:11:16.575Z] Blobファイルの処理を開始します
[2024-01-20T21:11:16.575Z]  Name:SampleImage.png 
[2024-01-20T21:11:16.575Z]  Size: 299338 Bytes
[2024-01-20T21:11:16.627Z] Executed 'ProcessFile' (Succeeded, Id=2e23e99d-1c40-4091-87e4-9e6407e5d57d, Duration=102ms)

Storage Explorerに戻ってみると、outputコンテナが作成され、inputにアップロードした画像がコピーされていることが確認できます。

Azure上で実行する

ローカル環境での動作確認が済みましたので、続いて、この関数アプリをAzure上で実行していきます。

そのために、次のリソースを作成します。

  • Azure Functions
  • Azure Blob Storage
  • Azure Key Vault

※ リソース作成は、全てAzure CLIで行います。

Blob Storageを作成する

まずは、ストレージアカウントを作成します。

az login
# リソースグループの作成
az group create --location japaneast --name test-func-project-rg
# ストレージアカウントの作成
az storage account create \
--resource-group test-func-project-rg \
--name testprojectblob20240121 \
--location japaneast \
--sku Standard_LRS

Portalで確認すると、該当のストレージアカウントが作成されています。

Functionsを作成する

続いて、Functionsです。

Functionsと関連付けするストレージを作成する

前述の通り、Functionsはその実行のためにストレージアカウントが必要です。Functions作成時には、関連付けするストレージアカウントの指定が必須となります。

そのため、Functionsを作成する前にFunctionsと関連付けするストレージを作成しておきます。

※ PortalからFunctionsを作成する場合、このストレージは自動的に生成されます。

az storage account create \
--resource-group test-func-project-rg \
--name testfuncstorage20240121 \
--location japaneast \
--sku Standard_LRS

Functionsを作成する

前項で作成したtestfuncstorage20240121をストレージアカウントに指定して、Functionsを作成します。

az functionapp create \
--name test-func-20240121 \
--resource-group test-func-project-rg \
--storage-account testfuncstorage20240121 \
--consumption-plan-location japaneast \
--runtime dotnet \
--functions-version 4

Portalで確認すると、Functionsが作成されています。

App ServiceプランがY1になっていたら、間違いなく従量課金プランです。

https://learn.microsoft.com/ja-jp/azure/azure-functions/consumption-plan#create-a-consumption-plan-function-app

Functions作成時にApp Serviceプランを明示的に作成する必要はありませんが、内部的に「Y1」と呼ばれるApp Serviceプランが作成されます。従量課金プランのFunctionsはこのApp Serviceプランの下で実行されます。

作成されたApp Serviceプランの詳細はPortalからも確認できます。

Key Vaultでシークレットを作成する

最後にKey VaultとBlobの接続文字列のシークレットを作成します。

Key Vaultリソースを作成する

az keyvault create \
--name kv-test-func-project \
--resource-group test-func-project-rg

シークレットを作成する

先ほど作成したBlobストレージtestprojectblob20240121の接続文字列を保持するシークレットを作成します。

シークレットは下記コマンドで作成できます。

az keyvault secret set \
--vault-name kv-test-func-project \
--name testprojectblob20240121-account-key \
--value "{blobストレージの接続文字列}を指定"

Blobの接続文字列は、Portalから確認できます。

Functionsの構成を更新する

FunctionsがBlobストレージにアクセスできるようにするため、Functionsの環境変数にTestProjectStorageを追加します。

TestProjectStorageには、Functions内で使用するストレージアカウントの接続文字列のKey Vaultシークレット(testprojectblob20240121-account-key)を指定します。

まず、シークレットのURI(シークレットの識別子)をPortalから確認します。

続いて、Functionsの環境変数にTestProjectStorageを登録していきます。

Functionsのアプリ設定でKey Vaultを参照する場合、以下の形式になります。

@Microsoft.KeyVault(SecretUri=シークレットのURI)

https://learn.microsoft.com/ja-jp/azure/app-service/app-service-key-vault-references?tabs=azure-cli#source-app-settings-from-key-vault

TestProjectStorage: @Microsoft.KeyVault(SecretUri=シークレットのURI)

という形式で環境変数を登録します。

Functionsのデプロイを行う

先ほどローカルで実行した関数アプリをデプロイします。

デプロイは、Azure Functions Core Toolsの以下のコマンドで実行できます。

func azure functionapp publish <作成したFunctionsの名前>
az login
# デプロイしたい関数アプリがあるディレクトリに移動してコマンドを実行します
cd func-app
# デプロイ
func azure functionapp publish test-func-20240121
Setting Functions site property 'netFrameworkVersion' to 'v6.0'
MSBuild のバージョン 17.7.4+3ebbd7c49 (.NET)
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  func-app -> /Users/******/projects/azure/func-app/bin/publish/func-app.dll

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:02.34

Getting site publishing info...
[2024-02-08T23:22:32.748Z] Starting the function app deployment...
Creating archive for current directory...
Uploading 5.53 MB [###########################################################]
Upload completed successfully.
Deployment completed successfully.
[2024-02-08T23:23:29.113Z] Syncing triggers...
Functions in test-func-20240121:
    ProcessFile - [blobTrigger]

PortalでFunctionsを見ると、ProcessFile関数がデプロイされたことを確認できます。

FunctionsがKey Vaultに安全にアクセスできるようにする

先ほど、Functionsの環境変数にシークレットの参照先を登録しました。しかし、これだけではFunctionsがKeyVaultのシークレットを参照することはできません。

KeyVaultへのアクセスには、適切なAzureADの認証が必要だからです。

https://learn.microsoft.com/ja-jp/azure/app-service/app-service-key-vault-references?WT.mc_id=AZ-MVP-5002209&tabs=azure-cli#grant-your-app-access-to-a-key-vault

ここでは、この認証プロセスを簡略化&セキュアにするため、マネージドIDを使います。

※ 構成図でいうと、赤枠の部分の話です。

具体的には、下記の2つを実施します。

  • FunctionsのマネージドIDを有効化する
  • マネージドIDに対して、KeyVaultへのアクセス権を付与する

これらを行うことで、FunctionsがKeyVaultのシークレット値(今回はBlobの接続文字列)を取得できるようになります。

FunctionsのマネージドIDを有効化する

該当のFunctions > 設定 > ID を開きます。

システム割り当て済みを選択し、状態をONにして保存します。

今有効化したFunctionsのマネージドIDは、Microsoft Entra ID > 管理 > エンタープライズアプリケーションで確認できます。

マネージドIDに対して、KeyVaultへのアクセス権を付与する

作成したKeyVaultのページに行き、アクセス制御 (IAM) > 追加 > ロールの割り当ての追加 を選択します。

ロールに、キー コンテナー シークレット ユーザーを選択します。

似たようなロールに、Key Vaultリーダーキー コンテナー閲覧者(Key Vault Reader)がありますが、これらのロールは、シークレットの参照ができませんので要注意です。

メンバーのアクセス割り当て先にマネージドIDを選択し、「メンバーを選択する」を押します。

先ほど有効化したFunctionsのマネージドIDを選択します。

「レビューと割り当て」を押します。

更新完了後、アクセス制御(IAM)を表示すると、キーコンテナー シークレットユーザーロールがFunctionsのマネージドIDに割り当たっていることを確認できます。

動作確認

ここまでの手順でAzureリソースの準備が完了しました。

Blobに画像をアップロードして、Functionsが動作するか確認してみます。

動かない

本来は、inputコンテナに画像をアップロードしたらoutputコンテナに同じ画像が複製されるはずです。

が、outputコンテナが作成されません😥

まず、Application insightsでログを確認してみます。

関数アプリ > 監視 > ログのページに行き、下記のクエリを実行します。

ちゃんとBlobトリガーでFunctionsが動作していれば、requestsにログが残ります。

しかし、BlobトリガーでFunctionsが動いたことを示すログは見当たりません。そもそもBlobトリガーが成功していない可能性が高そうです。

接続文字列の確認

BlobとFunctionsの接続に問題がありそうなことが分かったので、Functionsの環境変数に設定した接続文字列(TestProjectStorage)に誤りがないかを確認します。

この環境変数は、Key Vaultのシークレットを参照する形としていました。

どうやらここが怪しそうなので、試しにBlobストレージの接続文字列を直接指定してみます。

構成を変更したので、Functionsを再起動します。

この後、Blobストレージを見てみると、outputコンテナが作成されました。inputコンテナにアップロードした画像が複製されています。

リクエストのログも出ています。

原因はKey Vaultのアクセス許可モデル

接続文字列を直接指定したら想定通りの挙動となったので、原因はKey Vaultのシークレットの参照が上手くいっていないことにあるのが確定しました。

「おそらくシークレットに登録した値を間違えたか、付与したロールを間違えたのだろう」と思い、シークレット値、シークレットの参照方法、付与したロールを確認しましたが、いずれも誤っている箇所はありません🤔

そこで、改めてドキュメントを読んでみると、

Key Vault で Azure RBAC アクセス許可を有効にする

https://learn.microsoft.com/ja-jp/azure/key-vault/general/rbac-guide?tabs=azure-cli

ありました。

Key Vaultでロールベースのアクセス制御を使用する場合、アクセス許可モデルをコンテナーのアクセス ポリシー から Azureロールベースのアクセス制御に変更する必要があるようです。

規定ではコンテナーのアクセス ポリシーが選択されていました。

コンテナーのアクセス ポリシーが選択されている場合、このKeyVaultへのアクセス制御は「アクセスポリシー」で行われます。そのため、ロールベースのアクセス制御の設定をしても効果がないようです。

これをAzure ロールベースのアクセス制御に変更します。

改めて、inputコンテナに画像をアップロードします。

すると、outputコンテナに画像が複製されました🎉

今回掛かった料金

総額7.65円でした。

触ってみた程度の用途であれば、懐が痛むことはなさそうです😄

Blob Storageは使用したストレージ量に応じて課金されますので、画像の消し忘れにはご注意ください。今回は、基本的に最安プランを選択しました。

Discussion