🦖

Azure Functionsを利用したAzure Blob Storageへのファイルアップロード

2024/12/03に公開

1. この記事の範囲

Azure Functions は、サーバーレスアーキテクチャを活用した効率的なイベント駆動型アプリケーションの構築を可能にします。本記事では、Azure Functionsを使用し Azure Blob Storageにファイルをアップロードするサンプルコードを解説します。

2. 内容

2-1. 実装概要

Azure Functions を使用して、HTTP POST リクエストを受け取り、送信されたファイルを Azure Blob Storage にアップロードします。

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using MimeMapping;

namespace SampleFunctionApp
{
    public class UploadFileFunction
    {
        private readonly ILogger<UploadFileFunction> _logger;

        public UploadFileFunction(ILogger<UploadFileFunction> logger)
        {
            _logger = logger;
        }

        [Function("UploadFileFunction")]
         public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/files/upload")] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("UploadFileFunction triggered");

           try
            {
                // フォームデータを取得
                var form = await req.ReadFormAsync();
                var file = form.Files["file"];
                if (file == null || file.Length == 0)
                {
                    return new BadRequestObjectResult("No file uploaded or file is empty.");
                }

                // ファイルサイズ制限
                const long maxFileSize = 100 * 1024 * 1024; // 100MB
                if (file.Length > maxFileSize)
                {
                    return new BadRequestObjectResult($"File size exceeds the limit of {maxFileSize / (100 * 1024 * 1024)}MB.");
                }

                // ファイルのMIMEタイプを取得
                var contentType = MimeUtility.GetMimeMapping(file.FileName);

                // ストレージ接続文字列とコンテナ名の設定
                var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");
                var containerName = "your-container-name";

                // Blobクライアントを取得
                var blobServiceClient = new BlobServiceClient(connectionString);
                var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
                await containerClient.CreateIfNotExistsAsync(PublicAccessType.None);

                // フォルダ構造を利用してBlob名を生成 
                var folderPath = $"uploads/{DateTime.UtcNow:yyyyMMdd}";
                var blobName = $"{folderPath}/{Path.GetFileNameWithoutExtension(file.FileName)}_{DateTime.UtcNow:HHmmss}{Path.GetExtension(file.FileName)}";
                var blobClient = containerClient.GetBlobClient(blobName);

                using var stream = file.OpenReadStream();
                await blobClient.UploadAsync(stream, new BlobHttpHeaders { ContentType = contentType });

                // アップロード成功のレスポンスを返却
                return new OkObjectResult(new
                {
                    Message = "File uploaded successfully",
                    BlobUrl = blobClient.Uri.ToString()
                });
            }
            catch (Exception ex)
            {
                log.LogError($"Error in UploadFileFunction: {ex.Message}");
                return new ObjectResult(new { Error = "File upload failed", Details = ex.Message })
                {
                    StatusCode = StatusCodes.Status500InternalServerError
                };
            }
        }
    }
}

2-2. コードの詳細解説

2-2-1. ファイルアップロードのトリガー

[Function("UploadFileFunction")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = "api/files/upload")] HttpRequest req,
    ILogger log)

HTTP トリガー

  • HTTP トリガーは、Azure Functionsが HTTPリクエストを受信したときに実行されるトリガー。
  • 他のトリガー(例: Blob トリガー、キュー トリガー)とは異なり、クライアントから直接アクセス可能。
  • このコードでは、post を明示的に指定し、POSTリクエストのみを許可。必要に応じて複数のメソッドを許可可能です。
    [HttpTrigger(AuthorizationLevel.Function, "post", "put", Route = "api/files/upload")]
    

認証レベル
AuthorizationLevel.Function により関数キーが必要であり、セキュリティを確保しつつ簡単にアクセス制御が可能。他のオプションは以下。

  • Anonymous: 認証なし(推奨されません)
  • Admin: 管理者キーが必要で、最も厳しい認証レベル

2-2-2. フォームデータの取得と基本検証

var form = await req.ReadFormAsync();
var file = form.Files["file"];
if (file == null || file.Length == 0)
{
    return new BadRequestObjectResult("No file uploaded or file is empty.");
}

フォームデータの解析
ReadFormAsync を利用して、アップロードされたファイル (IFormFile) を取得し、ファイルが空の場合やアップロードが失敗した場合、400 Bad Request を返却。

2-2-3. ファイルサイズ制限

// ファイルサイズが100MBを超える場合、400 Bad Requestを返します。
const long maxFileSize = 100 * 1024 * 1024; // 100MB
if (file.Length > maxFileSize)
{
    return new BadRequestObjectResult($"File size exceeds the maximum limit of {maxFileSize / (1024 * 1024)}MB allowed by Azure Functions.");
}

Azure Functionsの場合
Azure Functionsの HTTP トリガーでは、リクエストボディのサイズ制限がデフォルトで100MBに設定されています。この制限は Azure Functions全体の動作を制御する host.json ファイルで変更可能です。ファイルサイズ制限をデフォルトの100MBより小さい値に変更したい場合、以下の手順を実行します。
host.json ファイルに以下の設定を追加し、maxRequestLength を適切な値(単位: バイト)に変更します。この設定はFunctionアプリ全体に適用されますが、100MB を超えるサイズには設定できません。以下の例では、リクエストボディのサイズ制限を50MBに設定しています。

{
  "version": "2.0",
  "extensions": {
    "http": {
      "maxRequestLength": 52428800 // 50MB
    }
  }
}

App Service プランの場合
App Serviceプランでホストしている場合、リクエストボディのサイズ制限は IIS(通常 4MB)に依存します。この制限はweb.config を使用して制限を拡張可能です。リクエストボディのサイズ制限をバイト単位で指定します(例: 52428800 = 50MB)。

<configuration>
  <system.webServer>
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="52428800" /> <!-- 50MB -->
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>

2-2-4. MIME タイプの判定

//ファイル拡張子から MIME タイプを取得
var contentType = MimeUtility.GetMimeMapping(file.FileName);

  • example.txttext/plain
  • 未知の拡張子 → デフォルト値 application/octet-stream

以下のようにFileExtensionContentTypeProviderを使用している場合

// ファイルのコンテンツタイプを決定
new FileExtensionContentTypeProvider().TryGetContentType(file.FileName, out string? contentType);
contentType ??= "application/octet-stream";

FileExtensionContentTypeProviderMicrosoft.AspNetCore.StaticFiles に依存しています。
Microsoft.AspNetCore.StaticFiles が非推奨になったため代替えとしてMimeMapping パッケージを使用します。

2-2-5. Azure Blob Storage の接続設定

// 環境変数から接続文字列を取得
var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");

テストコードのため接続文字列は local.settings.json で設定していますが、本番環境や安全性が求められる場面ではKey Vaultを使用します。

var connectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage");

// ローカル環境かどうかを判定
var isLocal = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"));

// 本番環境では Key Vaultから接続情報を取得
if (!isLocal)
{
    var keyVaultUrl = Environment.GetEnvironmentVariable("KeyVaultUrl");
    var secretClient = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
    var secret = await secretClient.GetSecretAsync("AzureWebJobsStorage");
    connectionString = secret.Value.Value;
}

var blobServiceClient = new BlobServiceClient(connectionString);

2-2-6. Blobクライアントの生成

var blobServiceClient = new BlobServiceClient(connectionString);
var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
await containerClient.CreateIfNotExistsAsync(PublicAccessType.None);

Blob Service クライアント

  • 接続文字列を使用して Azure Blob Storageサービスに接続します。
  • BlobServiceClient は、Blob Storage内のすべての操作の起点となるクライアントクラスです。接続文字列のほかに Azure AD認証やSAS トークンを使用することも可能です。

コンテナの存在確認と作成

  • 指定されたコンテナが存在しない場合、CreateIfNotExistsAsync を呼び出すことで自動的に作成します。
  • PublicAccessType.None を指定することで、コンテナへのパブリックアクセスを無効化し、セキュリティを確保します。
    パブリックアクセスの設定は3種類あります。
    • PublicAccessType.None:デフォルトでパブリックアクセスを無効化し、認証が必要な場合に利用。
    • PublicAccessType.Blob:コンテナ内の個別のBlobへの匿名アクセスを許可します。
    • PublicAccessType.Container:コンテナ全体への匿名アクセスを許可します(非推奨)

2-2-7. フォルダ構造を利用したBlob名の生成

var folderPath = $"uploads/{DateTime.UtcNow:yyyyMMdd}";
var blobName = $"{folderPath}/{Path.GetFileNameWithoutExtension(file.FileName)}_{DateTime.UtcNow:HHmmss}{Path.GetExtension(file.FileName)}";

フォルダ構造

  • Blob Storageは厳密にはフォルダ構造を持ちませんが、Blob名にパスのような形式を持たせることで仮想的なフォルダ構造を実現します。
  • フォルダのように見える形式を使うことで、データを論理的に整理できます。
    Blob 名
  • ファイル名にタイムスタンプ(HHmmss 形式)を追加することで、同じ日付内でも一意のBlob名を保証します。

既存Blobの上書きを防ぐ方法
Azure Blob Storageでは、同じBlob名でファイルをアップロードした場合、既存のBlobが上書きされます。もし上書きを許容する場合は、存在チェックを省略してそのままアップロードを実行できます。その場合、既存のBlobが新しいBlobに置き換えられます。

  1. 一意のBlob名を生成
    • GUIDをBlob名に追加して一意性を保証します。
    var blobName = $"{folderPath}/{Path.GetFileNameWithoutExtension(file.FileName)}_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
    
  2. Blobの存在チェック
    • ファイルをアップロードする前にBlob名が既に存在するかを確認します。
    var blobClient = containerClient.GetBlobClient(blobName);
    
    // Blobが既に存在するかをチェック
    if (await blobClient.ExistsAsync())
    {
        return new ConflictObjectResult(new
        {
            Message = "A file with the same name already exists.",
            ExistingBlobUrl = blobClient.Uri.ToString()
        });
    }
    

2-2-8. ファイルのアップロード

var blobClient = containerClient.GetBlobClient(blobName);

using var stream = file.OpenReadStream();
await blobClient.UploadAsync(stream, new BlobHttpHeaders { ContentType = contentType });

Blob クライアントの取得

  • containerClient.GetBlobClient(blobName) を使用して、指定されたBlob名に基づきクライアントを生成。これにより、特定のBlobに対する操作が可能になります。

ファイルのアップロード

  • blobClient.UploadAsync メソッドを使用して、ファイルストリームを非同期でBlobにアップロード。Azure Blob Storage SDK のメソッドは、I/O操作に最適化された非同期設計を採用しています。パフォーマンスを最大化するために非同期メソッドを使用してください。
  • BlobHttpHeaders を使用してMIMEタイプ(Content-Type)を設定することで、ダウンロード時に正しい形式が認識されます。

ファイルのアップロードに関する補足情報
Blob Storageを活用したファイルアップロードのパフォーマンスと信頼性を向上するには、いくつか方法があります。

1. アップロード中の進捗追跡
大容量ファイルをアップロードする場合、進捗を追跡することでユーザーに状況を伝えられます。

var progressHandler = new Progress<long>(bytesTransferred =>
{
    Console.WriteLine($"Uploaded {bytesTransferred} bytes.");
});

await blobClient.UploadAsync(stream, new BlobUploadOptions
{
    ProgressHandler = progressHandler
});

2.アップロードのリトライポリシー

  • ネットワークエラーなどが発生した場合にリトライする設定を追加できます。
  • Azure SDKには既定でリトライポリシーが組み込まれていますが、カスタム設定も可能です。
csharp
コードをコピーする
var options = new BlobClientOptions
{
    Retry =
    {
        MaxRetries = 5,
        Delay = TimeSpan.FromSeconds(2),
        Mode = RetryMode.Exponential
    }
};

var blobClient = new BlobClient(connectionString, containerName, blobName, options);

他にもAzure Blob Storage SDKの自動分割アップロード機能を利用し、大容量ファイルでも効率的に処理することを考えられます。

3. まとめ

この記事では、Azure Functionsを使用してAzure Blob Storageにファイルをアップロードする実装について詳しく解説しました。Azure FunctionsのHTTP トリガーを活用したファイルアップロードは、サーバーレスアーキテクチャの利点を最大限に活かし、スケーラブルで効率的な設計を可能にします。
運用環境では、ファイルサイズ制限やセキュリティ設定などの課題が考えられますが、Azure Functionsは従量課金モデルを採用しているため、使用した分だけのコストで運用でき、無駄がありません。特に、初めてサーバーレスやAzure Blob Storageを試す場合、この組み合わせは手軽で効果的な選択肢です。

Discussion