サーバーレス コンピューティングと Azure Functions の概要

本記事は、マイクロソフト本社の Scott Hanselman Blog の記事を抄訳したものです。 【元記事】 What is Serverless Computing? Exploring Azure Functions 2016/8/27

 

「クラウド」という言葉は別にしても、クラウドの世界はわかりづらい言葉であふれています。

  • IaaS (サービスとしてのインフラストラクチャ) - オンデマンドで使用可能な仮想マシンなどのリソース。
  • PaaS (サービスとしてのプラットフォーム) - アプリのデプロイ先。その基盤となる仮想マシンなどについて考慮する必要はありません。実際には仮想マシンは存在しているのですが、必要がない限り存在が見えないようになっています。
  • SaaS (サービスとしてのソフトウェア) - Office 365 や Gmail のような製品。サブスクリプション料金を支払って電子メールなどのサービスを利用します。ユーザーには動作しているようすのみが見えます。

「サーバーレス コンピューティング」では、実際にサーバーが存在しないわけではありません。サーバーレスとは、ユーザーがサーバーの存在を意識する必要がないという意味です。これは PaaS と類似していますが、さらに高レベルなものです。

サーバーレス コンピューティングでは、コードを作成してスライダーを調整し、クレジット カードを用意します。 以上で関数を使用する準備が完了し、支払い額に応じてスケーリングすることができます。これは、クラウドで得られる「クラウド的」なメリットによく似ています。

PaaS では、Node や C# のアプリを作成して Git にチェックインし、Web サイトにデプロイするか、または Web アプリケーションとしてデプロイすると、エンドポイントが作成されます。この場合、スケール アップ (CPU の高速化やメモリ、ディスクの容量増加) またはスケール アウト (Web アプリのインスタンス数の増加) は可能ですが、シームレスに実行することはできません。非常に有効な手段ではありますが、この環境では常にサーバーを意識する必要があります。

Amazon LambdaAzure Functions のような新しいクラウド システムでは、コードをアップロードした数秒後にはそれを実行できます。ここでは、継続的なジョブやイベントによりトリガーされる関数を実行したり、Web API や URL 付きの関数の Webhook を作成したりできます。

この記事では、サーバーレス コンピューティング環境で手軽に Web API を作成できる方法について説明します。

まず、https://azure.microsoft.com/ja-jp/services/functions/ にアクセスして新しい関数を作成します。アカウントをお持ちでない方は、無料でサインアップできます。

Getting started with Azure Functions

関数は JavaScript または C# で作成できます。

Getting started with Azure Functions - Create This Function

Azure Function エディターにアクセスしたら [New Function] をクリックします。下記のようなテンプレートやサンプル コードが数十種類用意されています。

  • 画像内から顔を検出し、その周辺の長方形を保存する
  • GitHub Webhook がトリガーされた場合に関数を実行し GitHub の issue にコメントを投稿する
  • HTTP 要求を受け取ったときにストレージ BLOB を更新する
  • データベース テーブルまたはストレージ テーブルからエンティティを読み込む

ここでは、上記の最初の例を変更することにします。この関数では、ストレージ内の画像を表示し、Cognitive Services の API を呼び出して顔の位置を取得し、データを保存するという動作をトリガーします。これを次のように変更します。

  • 画像を HTTP POST からの入力として取得する
  • 顔の周囲に長方形を描画する
  • 新しい画像を返す

この作業は Git や GitHub で行うことができますが、ここでは簡略化のためすべてブラウザーで行います。そのようすを次の図に示します。

コードを作成して繰り返し変更し、保存して、何度も実行してはエラーを発生させます。基礎として使用した最初のコードを次に示します。このコードはイベントをトリガーする最初の関数です。ここで、コード内の Run() を変更します。

 #r "Microsoft.WindowsAzure.Storage"
#r "Newtonsoft.Json"

using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Table;
using System.IO; 

public static async Task Run(Stream image, string name, IAsyncCollector<FaceRectangle> outTable, TraceWriter log)
{
    var image = await req.Content.ReadAsStreamAsync();
    
    string result = await CallVisionAPI(image); //STREAM
    log.Info(result); 

    if (String.IsNullOrEmpty(result))
    {
        return req.CreateResponse(HttpStatusCode.BadRequest);
    }

    ImageData imageData = JsonConvert.DeserializeObject<ImageData>(result);
    foreach (Face face in imageData.Faces)
    {
        var faceRectangle = face.FaceRectangle;
        faceRectangle.RowKey = Guid.NewGuid().ToString();
        faceRectangle.PartitionKey = "Functions";
        faceRectangle.ImageFile = name + ".jpg";
        await outTable.AddAsync(faceRectangle); 
    }
    return req.CreateResponse(HttpStatusCode.OK, "Nice Job");  
}

static async Task<string> CallVisionAPI(Stream image)
{
    using (var client = new HttpClient())
    {
        var content = new StreamContent(image);
        var url = "https://api.projectoxford.ai/vision/v1.0/analyze?visualFeatures=Faces";
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", Environment.GetEnvironmentVariable("Vision_API_Subscription_Key"));
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        var httpResponse = await client.PostAsync(url, content);

        if (httpResponse.StatusCode == HttpStatusCode.OK){
            return await httpResponse.Content.ReadAsStringAsync();
        }
    }
    return null;
}

public class ImageData {
    public List<Face> Faces { get; set; }
}

public class Face {
    public int Age { get; set; }
    public string Gender { get; set; }
    public FaceRectangle FaceRectangle { get; set; }
}

public class FaceRectangle : TableEntity {
    public string ImageFile { get; set; }
    public int Left { get; set; }
    public int Top { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
}

目標 : この Run() を変更し、画像を含む HTTP 要求をリッスンして POST された画像 (ここでは検証は行いません) を読み込み、検出された顔の周辺に長方形を描画し、新しい画像を返すようにします。

 public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log) {
    var image = await req.Content.ReadAsStreamAsync();

この関数の本体について、ちょっと MemoryStreams を多用しすぎているような気もしますが、これから整理することにして、このコードを初期概念実証バージョンとします。この後、少なくとも 2 つのことを行う必要があります。ともかく、この件に関してよくご存じの方とお話することができて、想像以上の優れたアイデアを得られました。次に示すように、基本的には API を呼び出して複数の顔のデータを取得します。

 2016-08-26T23:59:26.741 {"requestId":"8be222ff-98cc-4019-8038-c22eeffa63ed","metadata":{"width":2808,"height":1872,"format":"Jpeg"},"faces":[{"age":41,"gender":"Male","faceRectangle":{"left":1059,"top":671,"width":466,"height":466}},{"age":41,"gender":"Male","faceRectangle":{"left":1916,"top":702,"width":448,"height":448}}]}

このデータを取得して、顔が検出された場所の周辺に長方形を描画します。

 public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    var image = await req.Content.ReadAsStreamAsync();

    MemoryStream mem = new MemoryStream();
    image.CopyTo(mem); //他の API により破壊されるため、コピーを作成します。ただ、あまり良い方法ではありません。
    image.Position = 0;
    mem.Position = 0;
    
    string result = await CallVisionAPI(image); 
    log.Info(result); 

    if (String.IsNullOrEmpty(result)) {
        return req.CreateResponse(HttpStatusCode.BadRequest);
    }
    
    ImageData imageData = JsonConvert.DeserializeObject<ImageData>(result);

    MemoryStream outputStream = new MemoryStream();
    using(Image maybeFace = Image.FromStream(mem, true))
    {
        using (Graphics g = Graphics.FromImage(maybeFace))
        {
            Pen yellowPen = new Pen(Color.Yellow, 4);
            foreach (Face face in imageData.Faces)
            {
                var faceRectangle = face.FaceRectangle;
                g.DrawRectangle(yellowPen, 
                    faceRectangle.Left, faceRectangle.Top, 
                    faceRectangle.Width, faceRectangle.Height);
            }
        }
        maybeFace.Save(outputStream, ImageFormat.Jpeg);
    }
    
    var response = new HttpResponseMessage()
    {
        Content = new ByteArrayContent(outputStream.ToArray()),
        StatusCode = HttpStatusCode.OK,
    };
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
    return response;
}

次に System に参照を追加しました。ファイル冒頭でこの構文を使用して描画を実行し、ここで使用する System.Drawing や System.Drawing.Imaging などの名前空間を追加しました。また、入力について、[Integrate] タブの入力を "HTTP" に変更しました。

 #r "System.Drawing

それでは、Postman (英語) にアクセスして、新規作成された Azure Function のエンドポイントに画像を POST します。ここでは、実物よりもうまく撮れている私と、実物よりもうまく撮れていない The Oatmeal の設立者である Matthew Inman 氏の写真をアップロードしています。彼は、実際もっと素敵な男性なのですが。

特に何もせず 15 分ほどブラウザー、Postman (とブラウザー)、Google/StackOverflow、Azure Functions を眺めていると、バックエンドの概念実証が完了しました。

Azure Functions では、Node.js、C#、F#、Python、PHP の各言語、および Batch、Bash、PowerShell の各ツールをサポートしていて、ほぼすべてのユーザーが自由に使用できます。Slack にコードを送信したり、システムを自動化したり、GitHub の issue を更新したり、Webhook として動作させたりするなど、Web で関数を使用したいときには、どのようなケースでもこれを利用できます。また、こちらの GitHub リポジトリ (英語) ではサードパーティ製の優れた Azure Functions のサンプル コードが公開されています。入力も出力も、基本的に場所を選ばず行うことができます。そのため、スケーリングが簡単な「サーバーレス バックエンド」を Azure Tables や Azure Storage などのクラウド サービスでも利用できます。

まだわからないこともありますが、VM (完全に制御可能)、Web アプリ (ほぼ完全に制御可能)、「サーバーレス」の Azure Function (制御可能な範囲は狭いながらも、制御は不要でただ関数をクラウドで使用したい場合に便利) のいずれが適しているのか、場合に応じて判断できるようになりました。


スポンサー : Aspose により、DOC、XLS、PPT、PDF などの多数のファイルで API のプログラミングが可能になりました。同社の製品を使用して、ファイルの作成、変換、変更、管理をあらゆる方法で行うことができます。ぜひ Aspose の優秀な製品をご利用ください。こちらのページ (英語) で各製品の内容を確認し、無料試用版をダウンロードできます。

SCOTT HANSELMAN について

Scott Hanselman は、大学教授と金融会社のチーフ アーキテクトという経歴を持ち、現在はマイクロソフトの社員として講演やコンサルティングを担当しています。父親や糖尿病患者としての顔もあります。また、漫談やドレッドヘアーを編むことが好きで、執筆活動も行っています。