Azure FunctionsをVS2017でローカル実行&デバッグしてパブリッシュする

MSDNブログ(↓↓↓)で Azure Functions 関連の記事を見たので自分でやってみました。

blogs.msdn.microsoft.com

上記ブログに記述されている事、そして今回自分で試した事を要約すると以下の内容です。

  1. Azure FunctionsをVisual Studio 2017で開発&デバッグする(つまりローカル実行するということ)
  2. Azure Functionsをクラスライブラリとしてビルド&パブリッシュする

1 は、まあ 開発効率を上げるということですね。
Azure Functions自体はAzureポータルでコーディングできますが、本格的実装はローカルで行いたいですよね。

2 は、クラスライブラリとしてアセンブリ(dll)にしてデプロイすることで実行パフォーマンスを上げる意味を持ちます。
Azure Functionsは5分間非アクティブであるとアイドル状態となり、次の実行時には再度メモリ上に展開されます。C# / JavaScriptコードがデプロイされている場合、アセンブリへのコンパイル作業から再実行され起動パフォーマンスが落ちてしまいます。

では、以下に手順を・・・

ちなみに、Visual Studioとの統合ツール「Visual Studio Tools for Azure Functions」がまだ 2017 対応できていないらしく、そのため以下のような手動による操作がいくつか発生します。でも、あと数週間もすればツールがリリースされるみたいです。

Azure Functions CLI のインストール

Azure Functions CLI をインストールします。
以下のnpmコマンドでグローバル領域にインストールします。

npm i -g azure-functions-cli

www.npmjs.com

Visual Studio 2017 でWebプロジェクトを作成

Visual Studio 2017を起動します。
メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
プロジェクトテンプレートは「Web → ASP.NET Web アプリケーション(.NET Framework)」、プロジェクト名はここでは「AzureFunctionsWeb」としました。

f:id:daigo-knowlbo:20170323130918p:plain

「空」テンプレートを選択します。

f:id:daigo-knowlbo:20170323130947p:plain

プロジェクトのプロパティを表示します(ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックメニューで「プロパティ」を選択)。

f:id:daigo-knowlbo:20170323131619p:plain

Webタブを選択し、以下の設定を行います。
* 「外部プログラムを起動する」を選択。
* パスは「C:\Users\「【ユーザー名】\AppData\Roaming\npm\node_modules\azure-functions-cli\bin\func.exe」とする。
* 「コマンドライン引数」は「host start」とする。
* 「作業ディレクトリ」は【プロジェクトフォルダ】とする。

設定を行った画面は以下の通り。

f:id:daigo-knowlbo:20170323132050p:plain

Azure Portal上で Azure Functions を作成

Visual Studio Tools for Azure Functionsがリリースされれば、VSからプロジェクトを生成できると思いますが、それが今はないのでポータルで作成して、ローカルVSにベースとなるソースを落としてきます)

Azure Functions 作成画面を表示

https://functions.azure.com/signinをブラウザで開きます。
「New function app」の「Name」に作成するファンクション名を入力します。ここでは「RyuichiFunction0916」としました。
「Region」は「Japan East」としました。
「Create + get started」をクリックします。

f:id:daigo-knowlbo:20170323192122p:plain

「WebHook + API」「C#」を選択し、「この関数を作成する」ボタンをクリックします。

f:id:daigo-knowlbo:20170323192411p:plain

作成完了。

f:id:daigo-knowlbo:20170323192506p:plain

Azure Functions ソースのダウンロード

左下の「Function App の設定」をクリックします。

f:id:daigo-knowlbo:20170323192544p:plain

「Kuduに移動」をクリックします。

f:id:daigo-knowlbo:20170323192731p:plain

「site」を選択します。

f:id:daigo-knowlbo:20170323192817p:plain

「wwwroot」のダウンロードアイコンをクリックします。

f:id:daigo-knowlbo:20170323193106p:plain

ダウンロードしたzipのイメージは以下の通り。

f:id:daigo-knowlbo:20170323193250p:plain

WebプロジェクトにFunctionsソースを追加

先程作成したWebプロジェクト(AzureFunctionsWeb)フォルダにzipの内容を解凍します。

f:id:daigo-knowlbo:20170323193453p:plain

追加のファイルをプロジェクトに含めます。
また「run.csx」ファイルを「run.cs」にリネームします。

f:id:daigo-knowlbo:20170323193742p:plain

run.csを以下のように修正します。
run.csxでは単純に run() だけが定義されていますが、csコードにする為「名前空間・クラス」でくくります。
また必要な using を追加します。
レスポンス文字列も若干修正を加えました。

// run.cs
using Microsoft.Azure.WebJobs.Host;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace AzureFunctionsWeb
{
  public class ExampleFunction
  {
    public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // parse query parameter
      string name = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
        .Value;

      // Get request body
      dynamic data = await req.Content.ReadAsAsync<object>();

      // Set name to query string or body data
      name = name ?? data?.name;

      return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, 
                             "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK,
                             "Hello " + 
                              name + 
                              ". I'm assembly!!");
    }
  }
}

プロジェクトにNugetパッケージを追加

Nugetで以下のパッケージを追加します。

function.jsonの調整

function.jsonに「scriptFile」「entryPoint」キーを追加します。
scriptFileにはアセンブリ名を、entryPointには名前空間/クラス付きのRun()メソッドの完全名を記述します。

// function.json
{
  "scriptFile": "..\\bin\\AzureFunctionsWeb.dll",
  "entryPoint": "AzureFunctionsWeb.ExampleFunction.Run",
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

ローカル実行

Visual StudioでF5クリックにより実行を行います。
以下のようなコマンドプロンプトが表示され Function がリクエスト待ち(実行待ち)状態となります。

f:id:daigo-knowlbo:20170323195843p:plain

コマンドプロンプト表示上に、以下の記述を確認することができます。

Job host started
Http Function HttpTriggerCSharp1: http://localhost:7071/api/HttpTriggerCSharp1

このURLがFunctionの受け口のURLになります。

サンプル(ExampleFunction)は「name」というキーをパラメータとして受け取ります。その為、以下のURLをブラウザでアクセスしてみましょう。

http://localhost:7071/api/HttpTriggerCSharp1?name=daigo

Functionが実行され以下のようなレスポンスが得られます。

f:id:daigo-knowlbo:20170323200130p:plain

ブレークポイントもきちんと止まります。

f:id:daigo-knowlbo:20170323200300p:plain

Azure FunctionsへのPublish

次に、ローカルで実装を行った Function を AzureにPublishします。
コマンドプロンプトを起動します。

作成したWebプロジェクトフォルダ(F:\Projects\Research\AzureFunctionsWeb\AzureFunctionsWeb)に移動します。

以下のコマンドを実行してCLIからAzureにログインします。

func azure login  

Azureへのログイン画面が表示されるので、ログインを行います。

f:id:daigo-knowlbo:20170323200613p:plain

以下のコマンドで、現在PublishされているFunctionの一覧を確認します。

func azure functionapp list

現在の私の環境では2つのFunctionがPublishされていました。
RyuichiFunction0916の方が、今回作成したものです。

f:id:daigo-knowlbo:20170323200744p:plain

以下のコマンドによりFunctionをローカルからAzureへPublishします。
(RyuichiFunction0916部分は適時 Function 名に置き換えます)

func azure functionapp publish RyuichiFunction0916

正常にPublishされた結果画面が以下になります。

f:id:daigo-knowlbo:20170323200919p:plain

Azure Functionsを実行

Azureポータルで「RyuichiFunction0916」を表示。
「開発」タブを選択、「テスト」タブを選択、「要求本文」をテスト文字列に修正、「実行ボタン」クリック、を行います。
以下のように「出力」に「"Hello Daigo. I’m assembly!!“」が表示されました。
VS2017上で実装したロジックが実行されていることが確認できます。

f:id:daigo-knowlbo:20170323202035p:plain

まとめ

VS2017用「Visual Studio Tools for Azure Functions」がリリースされれば、上記のようなゴニョゴニョした作業はバックエンドで自動化されると思います。ただ、CLIなんかで、裏の動きを知っておくのは良いことだと思っています。
また、Azure functionsやマイクロサービスについては、私自身はまだ実運用の場面では使ったことがないので、色々な知見を経験者の方から学びたいと思っております。
WebHookを絡めてGithubだったりSlackだったりと連携する実装なんかも比較的容易に出来るようなので、色々試してみたいと思います。