かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Azure Functions の Proxies が GA したので試してみよう!

こちらの記事は、Qiita に掲載した Microsoft Azure Tech Advent Calendar 2017 の企画に基づき、執筆した内容となります。 カレンダーに掲載された記事の一覧は、こちらよりご確認ください。

はじめに

みなさん Azure Functions 使ってますか? Visual Studio 2017 での開発ツールも強化されて段々といい感じになってきました。

blogs.msdn.microsoft.com

Azure Functions v2 に対応していたり、プロジェクト作成時に、どのストレージアカウント使うか選べたり、色々いい感じになってますね!

本題

ということで、先日 Azure Functions の Proxies が GA しました。

blogs.msdn.microsoft.com

Proxies の定義は Microsoft Azure Portal でグラフィカルに設定が出来るようになっています。

f:id:okazuki:20171206112859p:plain

このほかに CLI から proxy を追加したりできます。

例えばこんなコマンドをうつと…

func proxy create --name MyProxy --route hello --methods GET --backend-url https://www.bing.co
m

こういう proxies.json が作られます。

{
  "Proxies": {
    "MyProxy": {
      "matchCondition": {
        "methods": [
          "GET"
        ],
        "route": "hello"
      },
      "backendUri": "https://www.bing.com"
    }
  }
}

その他に、Visual Studio で proxies.json を開くと、こんな感じにインテリセンスが効いてくれるので json で書くのも捗ります。

f:id:okazuki:20171206113548p:plain

f:id:okazuki:20171206113623p:plain

Azure Functions Proxies で出来ること

完全な説明は以下のドキュメントを見てください。

docs.microsoft.com

個人的に主な用途として以下のものが思いつきます。

  • とっちらった API を Proxies で綺麗に見せる
  • モック API を作る

ちょっと見てみたいと思います。Azure Functions 単体でも HttpTrigger を使って Http に応答する API は割と簡単に作ることが出来ます。例えば Function1 は GET リクエストを受け取って指定された id パラメータのオブジェクトをとってきて JSON で返すみたいなやつですね。

using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace DemoFuncApp
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            string id = req.GetQueryNameValuePairs()
                .FirstOrDefault(q => string.Compare(q.Key, "id", true) == 0)
                .Value;

            if (string.IsNullOrEmpty(id))
            {
                return req.CreateErrorResponse(HttpStatusCode.BadRequest, "id is required.");
            }

            return req.CreateResponse(HttpStatusCode.OK, new { id = id, name = $"sample data {id}" });
        }
    }
}

さらに Function2 は、POST メソッドを受け取って body の JSON の内容を元にリソースを作ってレスポンスで作った結果のオブジェクトを JSON 形式で返すみたいな。

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace DemoFuncApp
{
    public static class Function2
    {
        [FunctionName("Function2")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            var name = (string) (await req.Content.ReadAsAsync<dynamic>()).name;
            if (string.IsNullOrEmpty(name))
            {
                return req.CreateErrorResponse(HttpStatusCode.BadRequest, "name is required.");
            }
            return req.CreateResponse(
                HttpStatusCode.Created, 
                new { id = Guid.NewGuid().ToString(), name = name });
        }
    }
}

こうして作成した HttpTrigger の関数は /api/Function1 や /api/Function2 といったような個別の URL でアクセスできます。

これでもいいんですが、同じリソースに対してアクセスするなら /api/people みたいなエンドポイントに対する GET や POST で呼び分けたいところです。そういう時に Proxies が使えます。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "people-get": {
      "matchCondition": {
        "route": "/api/people/{id}",
        "methods": [ "GET" ]
      },
      "backendUri": "https://%APP_NAME%.azurewebsites.net/api/Function1?id={id}"
    },
    "people-post": {
      "matchCondition": {
        "route": "/api/people",
        "methods": [ "POST" ]
      },
      "backendUri": "https://%APP_NAME%.azurewebsites.net/api/Function2"
    }
  }
}

matchCondition で同じエンドポイントなんだけど GET の場合と POST の場合で呼び出す backendUri を変えています。GET のほうでは、id を URL の埋め込む感じで受け取って、後ろに流すときにクエリ文字列に渡すようなこともしてます。 また、ここで使っている %APP_NAME% は、Function App のアプリケーション設定で APP_NAME 定義した値をひっぱってくるという書き方になります。こうすることで、例えばデプロイする Function App が変わっても proxies.json を変えずにアプリケーション設定をいじるだけで対応可能になります。 このほかには、アプリケーションを呼び出すときに必要な認証キーとかをアプリケーション設定からとってくるといった使い方があります。

これをデプロイして Postman あたりで叩いた結果が以下になります。

f:id:okazuki:20171206115208p:plain

f:id:okazuki:20171206115126p:plain

ちゃんと proxies.json で定義した URL に対しての呼び出しなのに結果は Function1 / Function2 で書いた処理の結果になっていることがわかりますね!こういった感じで、Function App の HttpTrigger で作った関数を REST API っぽくまとめあげることが出来ます。

このほかに、こういった REST API を開発するときってテスト用のモック API の準備とかが大変だと思うのですが、これも proxies.json で出来たりします。

例えば /api/mock でアクセスしたら、パラメータで受け取った値を使った適当な文字列を返すようなモック API は以下のように書けます。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "people-get": {
      "matchCondition": {
        "route": "/api/people/{id}",
        "methods": [ "GET" ]
      },
      "backendUri": "https://%APP_NAME%.azurewebsites.net/api/Function1?id={id}"
    },
    "people-post": {
      "matchCondition": {
        "route": "/api/people",
        "methods": [ "POST" ]
      },
      "backendUri": "https://%APP_NAME%.azurewebsites.net/api/Function2"
    },
    "mock": {
      "matchCondition": { "route": "/api/mock" },
      "responseOverrides": {
        "response.body": "Hello {request.querystring.name}",
        "response.headers.Content-Type": "application/json"
      }
    }
  }
}

最後に追加されている mock という定義ですね。responseOverrides を書くことで任意のレスポンスを返すことが出来るようになっています。今回は単純な文字列を返していますが、ここを JSON のオブジェクトにすることで複雑な JSON も返すことができるようになっています。

やってみましょう。

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "people-get": {
      "matchCondition": {
        "route": "/api/people/{id}",
        "methods": [ "GET" ]
      },
      "backendUri": "https://%APP_NAME%.azurewebsites.net/api/Function1?id={id}"
    },
    "people-post": {
      "matchCondition": {
        "route": "/api/people",
        "methods": [ "POST" ]
      },
      "backendUri": "https://%APP_NAME%.azurewebsites.net/api/Function2"
    },
    "mock": {
      "matchCondition": { "route": "/api/mock" },
      "responseOverrides": {
        "response.body": {
          "results": [
            {
              "id": 1,
              "name": "test1"
            },
            {
              "id": 2,
              "name": "test2"
            },
            {
              "id": 3,
              "name": "test3"
            }
          ]
        },
        "response.headers.Content-Type": "application/json"
      }
    }
  }
}

Postman で叩いてみると…

f:id:okazuki:20171206115955p:plain

ばっちりですね!

まとめ

Azure Functions Proxies が GA されたことで、Azure Functions での REST API の開発が捗りそうです!

この他に blob のファイルに転送するような proxy の定義を追加することで Function App + Blob で SPA なページとバックエンドの API がさくっと出来てしまいます。ここらへんはテクニカルエバンジェリストの牛尾さんも試されてます。

qiita.com

Durable Functions という面白い機能も Preview ですが追加されてきてて、よりたくさんのことが Functions で実現できるようになってきてますね!これについては、このアドベントカレンダーの初日の記事で今村さんが書かれてるので見てみてください。

[Advent Calendar 2017 Day1] Durable Functions ことはじめ – Japan Azure Cloud Integration Engineering

それでは、良い Azure Functions 生活を!