EntityFramework Core 2.2 + Cosmos DB ~ ファーストステップ

1. はじめに

2018年10月(?)あたりからPreview版とはいえ、EntityFramework CoreからCosmos DBにアクセスするプロバイダが提供されていたという事で試してみました。
データの保存と読み込みを行うだけの超基本となるファーストステップの記事になります。

使用した環境

2. こんな事をするよ

すごく単純に EF Core Cosmos DB Provider を使って単純なモデルクラスの保存と読み込みを行います。

3. Cosmos DBの作成

Azureポータルの「+リソースの作成」からCosmos DBを作成します。

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

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

4. 実装

以下順に。

4.1. プロジェクト作成

新規プロジェクトを作成します。
コンソール アプリ(.NET Core)

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

プロジェクト名は「EfCoreCosmosExamConsole」

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

4.2. Nuget追加

Nugetパッケージで「microsoft.EntityFrameworkcore.Cosmos」を追加します。
プレリリース版を含めるにして検索してインストールします。

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

4.3. モデルクラス / DbContextクラス追加

Cosmos DBに保存するデータモデルクラスを追加します。
FamilyクラスとPersonクラスとします(家族情報を保存するイメージ)。
※手抜きして Models/Family.cs の1ファイルに2つのクラスを定義しています。

// Models/Family.cs
using System;
using System.Collections.Generic;

namespace EFCoreCosmosExamConsole.Models
{
  public class Family
  {
    public Guid FamilyId { get; set; }

    public Person HeadOfHousehold { get; set; }

    public Person Partner { get; set; }

    public List<Person> Children { get; set; }
  }

  public class Person
  {
    public Guid PersonId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateTime Birth { get; set; }
  }
}

FamilyクラスがPersonクラスを世帯主(HeadOfHousehold )、パートナー(Partner)、子供(Children)として保有しています。

DbContextクラスを作成します。
RDBに対するEF Coreの時とほぼ同様の感じです。

// Models/PeopleContext.cs
using Microsoft.EntityFrameworkCore;

namespace EFCoreCosmosExamConsole.Models
{
  public class PeopleContext : DbContext
  {
    public DbSet<Family> Families { get; set; }

    public DbSet<Person> Persons { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseCosmos(
        "https://ryuichi111cosmos.documents.azure.com:443/",
        "ひみつひみつひみつひみつひみつひみつひみつひみつ",
        "PeopleDatabase"
      );
    }
  }
}

Cosmos DBプロバイダ固有の設定は「OnConfiguring()」での「UseCosmos()」呼び出しになります。
UseCosmos()メソッドは、Microsoft.EntityFrameworkCore.CosmosアセンブリMicrosoft.EntityFrameworkCore.CosmosDbContextOptionsの拡張メソッドとして定義/実装されています。
第1引数には接続先Cosmos DBのURL、第2引数には接続キー、第3引数には(任意の)データベース名を指定します。
「第1引数 接続先Cosmos DBのURL」「第2引数 接続キー」はAzureポータルの対象Cosmos DBのKeysで確認することができます。

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

ここまでで、ソリューションエクスプローラ的には↓↓↓こんな感じになる。

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

4.4. main()からDbContext呼び出し

Program.cs main()にCosmos DBへのデータ保存と読み込みを記述します。
RDBに対するEF Core実装とほぼ同じです。

// Program.cs
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using EFCoreCosmosExamConsole.Models;

namespace EFCoreCosmosExamConsole
{
  class Program
  {
    static void Main(string[] args)
    {
      // テスト用の家族オブジェクトを作成(世帯主+パートナー+子供×2)
      Person takashi = new Person() { PersonId = Guid.NewGuid(), FirstName = "Takashi", LastName = "Tanaka", Birth = new DateTime(1985, 4, 5) };
      Person sawako = new Person() { PersonId = Guid.NewGuid(), FirstName = "Sawako", LastName = "Tanaka", Birth = new DateTime(1983, 7, 12) };
      Person chiyori = new Person() { PersonId = Guid.NewGuid(), FirstName = "Chiyori", LastName = "Tanaka", Birth = new DateTime(2001, 10, 4) };
      Person mamoru = new Person() { PersonId = Guid.NewGuid(), FirstName = "Mamoru", LastName = "Tanaka", Birth = new DateTime(2002, 11, 20) };
      Family family = new Family()
      {
        FamilyId = Guid.NewGuid(),
        HeadOfHousehold = takashi,
        Partner = sawako,
      };
      family.Children = new List<Person>();
      family.Children.Add(chiyori);
      family.Children.Add(mamoru);

      // CosmosDBに保存
      using (var context = new PeopleContext())
      {
        // Database / Collectionを(無ければ)作成
        context.Database.EnsureCreated();

        // Familyをコンテキストに追加
        context.Families.Add(family);

        // SaveChanges()の裏側でオブジェクトをJSON変換、シャドウプロパティの追加、CosmosDBへの保存、が行われる
        context.SaveChanges();
      }

      // CosmosDBから読み込み
      using (var context = new PeopleContext())
      {
        // Familyを関連オブジェクトごと取得
        var loladedFamily = context.Families
          .Include(f => f.HeadOfHousehold)
          .Include(f => f.Partner)
          .Include(f => f.Children)
          .Where(f => f.FamilyId == family.FamilyId).FirstOrDefault();

        // Personを単独で取得
        var loadedSawako = context.Persons
          .Where(p => p.PersonId == sawako.PersonId).FirstOrDefault();
      }
    }
  }
}

テスト用に Family / Person モデルクラスの作成

Cosmos DBに保存するテスト用のモデルクラスはべた書きで作成しています。

Cosmos DBの初期化

AzureポータルからCosmos DBの入れ物は作成済みですが、それに紐づく「データベース」「コレクション」がまだ作成されていません。
以下の呼び出しを行うと「データベース」「コレクション」が存在しなければ作成してくれます。

context.Database.EnsureCreated();

ちなみにEnsureCreated()呼び出し直後に、Azureポータル Data Explorer で確認した状態は以下です。

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

データベース名=PeopleDatabase(PeopleContextにおいてUseCosmos()の第3引数で指定した名称)
コレクション名=PeopleContext(PeopleContextのクラス名)
Throughput(RU) = 400

Familyオブジェクトの保存

以下の呼び出しでCosmos DBにデータを保存することができます(RDBに対するEF Coreと全く同じ)。

context.Families.Add(family);
context.SaveChanges();

以下のように、同一のコレクション(PeopleContext)内に Family / Person オブジェクトが保存されます。

↓↓↓Family f:id:daigo-knowlbo:20190108020510p:plain

↓↓↓Person f:id:daigo-knowlbo:20190108020536p:plain

↓↓↓Person f:id:daigo-knowlbo:20190108020601p:plain

EF Core側の実装における DbContextの括りでコレクションが作成され、そこに対象DbContextが扱うオブジェクトが保存されます。
Cosmos DBにおいては「コレクション=RUの括り(=コスト/パフォーマンス)」であるため、実運用上はコストとパフォーマンスの兼ね合いでDbContextのくくりを検討することになると思います。

それから Family / Person クラスで定義していないプロパティが多数Cosmos DB側のJsonデータには含まれていますが、これらはCosmos DB側で保持するシャドウプロパティになります。

Familyオブジェクト / Personオブジェクトの読み込み

データの読み込みもRDBに対するEF Coreと全く同様です。

// Familyを関連オブジェクトごと取得
var loladedFamily = context.Families
  .Include(f => f.HeadOfHousehold)
  .Include(f => f.Partner)
  .Include(f => f.Children)
  .Where(f => f.FamilyId == family.FamilyId).FirstOrDefault();

// Personを単独で取得
var loadedSawako = context.Persons
  .Where(p => p.PersonId == sawako.PersonId).FirstOrDefault();

まとめ

EF Core Cosmos DB Providerは、RDB操作と非常に類似した(同様の)コードでCosmos DBへのアクセスが可能でいい感じですね。
かつてLINQがデータソースに依存しない(オンメモリオブジェクトであろうがRDBデータであろうが)プログラミングモデルを目指し、実現しましたが、そんなテイストで 相手がRDBであろうがCosmos DBであろうが同様のプログラムコードが書けるのはうれしいです。
もちろんアーキテクチャ的にはバックエンドの技術知識を持つ必要がありますが、すごく期待できるデータプロバイダな気がしました^^

サンプルコードは一応↓↓↓↓↓です。 github.com

(Durable Functions)「第22回 Azureもくもく会」に参加+LTした話

久しぶりに kingkino@マンダム (@kingkinoko) on Twitterさん主催のAzureもくもく会に参加しました。
で、LTもさせていただきました。

1. 何を もくもく+LT したの?

「Durable Functionsの基礎学習」とその発表でした。
(何かを作り上げた!みたいなものではないのだけれど、昔から技術のバックグラウンドを調べたりするの好きなので)

今年に入ってから、仕事では .NET/Azure を離れ、Java/AWS の世界に移っていましたが、ぽろぽろと趣味で.NET/Azureは やっていました。
で、先日のGlobal Azure Boot Camp 2018に参加してAzure界隈の方々とお話しさせていただいたら「Durable Functions」がホットだということで。
GW終わり辺りからドキュメントを読み、もくもく+LTをさせていただいたという感じです。

2. 発表資料

発表資料は↓↓↓です。

speakerdeck.com

なのですが、「学んだこと」「伝えたかった事」のメインはDemoをさせていただいた部分でした。
加えて、「内容がLT・プレゼン向きじゃない」+「私の説明が決して上手くない」ということで、伝わりにくかったかと思います。

ということでDemoした部分について、ブログらせて頂こうかと・・・

3. 何を学んだのか?何をDemoしたのか?

MS公式のDurable Functionsの資料を読んだ結果、以下のドキュメントがDurable Functionsが Durable である所以的な部分を指しているような気がしました。

docs.microsoft.com

しかし、
「ドキュメントに記述されているルールに従って実装すれば、いい感じにDurable Functionsは動くのだろうけど、何故上手く動くのかが腑に落ちない」
という感覚を持ち、その もやもや を払拭するための技術探索を行いました。
つまり、以下のような内容が、話としては理解するけれど、具体的にそうなる根拠となるバックエンドの知識が欲しい、と。。。

・オーケストレーター関数
 決定論的である必要がある。
 複数回実行されても結果が同じでなければならない。
  →現在日付の取得・GUIDのランダム生成の実行はNG。
  →DateTime.Now ではなく DurableOrchestrationContext.​Current​Utc​Date​Time を使う
・アクティビティ関数
 非決定論的操作が可能。
 ある責務を持ったドメイン ファンクションはここで定義。
  →つまり「DBアクセス」などの処理はアクティビティ関数に実装
・イベントソーシング(パターン)で実装されている。
・Azure Storage(キュー・テーブル)に実行履歴をチェックポイントする事で信頼性を得ている。
・awaitが呼び出されるとオーケストレーター関数をゼロから再実行する。

4. Demo内容

Durable Functionsがどのようなフローで実行されるのか?Azure Storageを使ってどのようにDurableに実行されるのか?(直訳すれば "丈夫に" "恒久的に"ですね)
ほぼVSテンプレが吐き出す以下のコードで検証します(Http TriggerによるDurable Functionsコード)。
カスタムしたのは 19 / 20行目 を追加したぐらいでしょうか。

01: using System;
02: using System.Collections.Generic;
03: using System.Net.Http;
04: using System.Threading.Tasks;
05: using Microsoft.Azure.WebJobs;
06: using Microsoft.Azure.WebJobs.Extensions.Http;
07: using Microsoft.Azure.WebJobs.Host;
08: 
09: namespace FunctionApp2
10: {
11:   public static class Function2
12:   {
13:     [FunctionName("Function2")]
14:     public static async Task<List<string>> RunOrchestrator(
15:       [OrchestrationTrigger] DurableOrchestrationContext context)
16:     {
17:       var outputs = new List<string>();
18: 
19:       var currentUtc = context.CurrentUtcDateTime;
20:       var current = DateTime.Now; // ※本当はオーケストレーター関数で実行しちゃいけないやつ
21: 
22:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Tokyo"));
23: 
24:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Seattle"));
25: 
26:       outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "London"));
27: 
28:       return outputs;
29:     }
30: 
31:     [FunctionName("Function2_Hello")]
32:     public static string SayHello([ActivityTrigger] string name, TraceWriter log)
33:     {
34:       log.Info($"Saying hello to {name}.");
35:       return $"Hello {name}!";
36:     }
37: 
38:     [FunctionName("Function2_HttpStart")]
39:     public static async Task<HttpResponseMessage> HttpStart(
40:       [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req,
41:       [OrchestrationClient]DurableOrchestrationClient starter,
42:       TraceWriter log)
43:     {
44:       // Function input comes from the request content.
45:       string instanceId = await starter.StartNewAsync("Function2", null);
46: 
47:       log.Info($"Started orchestration with ID = '{instanceId}'.");
48: 
49:       return starter.CreateCheckStatusResponse(req, instanceId);
50:     }
51:   }

ブレークポイントは張りまくります。

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

テストではローカルAzure Storage(Emu)を使いますが、中身を完全に空にしておきます。

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

4.1. 実行

(1) デバッグ実行

プロジェクトをデバッグ実行します。
以下のような感じでローカルでAzure Functionsが実行されます。

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

ここで、Azure Storage ExplorerでStorageの確認を行います。

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

「Blob Contaners」「Queues」「Tables」に色々作成されています(中身は空)。

※上記Storage項目のそれぞれの説明はここらへんに詳細が書かれています。構成により調整が可能。

(2) HTTPトリガーをキック

HTTPトリガーの受け口である「http://localhost:7071/api/Function2_HttpStart」をポストマンでキックします。

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

当然、FunctionsのHTTPトリガーである「HttpStart()」が呼び出されます。

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

「続行(F5)」を行います。
すると、コードの実装の通り「StartNewAsync("Function2")」でFunction2オーケストレーター関数が呼び出されます。
ということで、以下の画面のように Function2オーケストレーター関数 でブレークします。

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

再び「続行(F5)」して「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」まで飛んだ状態が以下です。

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

日付取得結果は以下の通りでした。

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:30:46

今度は「ステップオーバー(F10)」します。
CallActivityAsync()により Function2_Hello が呼び出され、以下の場所でブレークします。

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

この状態でAzure Storage Explorerにて「DurableFunctionsHubHistoryテーブル」を確認すると以下のようレコードが作成されています。

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

※要するに「Function2オーケストレーター関数→Function2_Helloアクティビティ関数の呼び出しの履歴の保存」がAzure Storageに対して行われたということです。

で、「続行(F5)」しちゃいます。
結果、ブレークするのはココ↓↓↓↓↓。

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

そう、次のアクティビティ関数呼び出しの個所ではなく、オーケストレーター関数の頭のブレークに飛びます。

また、「続行(F5)」しちゃいましょう。
こんな感じになりますね↓↓↓。

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

context.CurrentUtcDateTimeの値 → 2018/5/12 14:30:29
DateTime.Nowの値 → 2018/5/12 23:32:31

DateTime.Nowは実行した時間そのものが取得されているのに対し、context.CurrentUtcDateTime値は「初回実行時と同じ値」が取得されています!
何度実行しても(何度再生されても)結果が同じ、つまり「決定論的動作」をしています。オーケストレーター関数の条件を満たしている!

次に「ステップオーバー(F10)」すると、先程1度実行済みの「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Tokyo"));」が実行されるはず・・・
では「ステップオーバー(F10)」してみます。
Function2_Helloアクティビティ関数は呼び出されず、次のアクティビティ関数呼び出し箇所「outputs.Add(await context.CallActivityAsync("Function2_Hello", "Seattle"));」に移りました。

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

さらに「ステップオーバー(F10)」してみます。
今度は、Function2_Helloアクティビティ関数が呼び出されました(引数nameはもちろんSeattle)。

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

ここで再びAzure Storage Explorerを見てみましょう。
レコードが増えましたね。Function2_Helloアクティビティが2回呼ばれた記録が行われています。

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

また、Result列に「Hello Tokyo!」と、1つ目のアクティビティ関数の処理結果が保存されています。

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

ここでちょっといたずら的にTokyoをOsakaにUpdateしてしまいます。

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

再び「続行(F5)」しちゃいましょう。
想像通り、オーケストレーター関数のトップに帰ってきます。

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

F5を連打し、Durable Functionsの処理をすべて終了させます。

結果として処理の履歴はDurableFunctionsHubHistoryテーブルに以下のように出力されました。

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

DurableFunctionsHubInstancesテーブルを見ると、処理結果が保存されています。

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

ポストマンでstatusQueryGetUriをリクエストしてみると・・・

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

先程いたずらでAzure Storageデータを変更した「Osaka」の文字が。
つまり、ロジックをメモリ上で実行して終わりではなく、Azure Storageに処理状態を永続化していることが理解できました。

5. まとめ

長々と分かりにくい検証を行いましたが以下のことが明確に理解できたのではないかと思います。

  • Durable Functionsは、アクティビティ関数の実行履歴を細かくAzure Storageに保存している。
  • Durable Functionsは、オーケストレーター関数で定義されたアクティビティ関数を高い信頼性で実行するためにawaitのタイミングでAzure Storageに状態を保存し、自らをリプレイしてすべてのアクティビティ関数をワークフローとして実行している。

Durable Functionsの実装については以下のgithubで公開されているので、更にソースレベルで探索ができるのではないかと思います。

github.com

また、勉強会等でもkingkino@マンダム (@kingkinoko) on Twitterさんや(「🍖・ω・)「🍺 (@yu_ka1984) on Twitterさん などDurable Functionsマスターがいらっしゃるので、色々伺えるのではないかと思います。

Azure FunctionsでCosmos DBを入力バインドする

以前「Azure FunctionsからCosmos DBに出力バインドする」という記事を書きましたので、それと対になる形で入力バインドについても簡単にまとめておこうと思います。

ryuichi111std.hatenablog.com

今回 実装することは以下の通りです。

  • HttpTriggerを持つAzure Functionsを作成
  • Cosmos DBドキュメントを入力バインドで受け取る
  • Function内の処理で、受け取ったCosmos DBドキュメントに変更を加える

1 開発環境

利用する環境は、以下の通りです。

2 前提条件

以下のような Cosmos DB アカウント を用意している前提とします。

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

  • APIモデルは「DocumentDB」
  • データベース名「CompanyDB」、コレクション名「EmployeeCollection」を作成済
  • ドキュメントが1件登録済み(id=“0000000001")
-- 登録済みドキュメント --
{
  "id": "0000000001",
  "firstname": "ryuichi",
  "lastname": "daigo",
  "_rid": "5T9QALZ5bQABAAAAAAAAAA==",
  "_self": "dbs/5T9QAA==/colls/5T9QALZ5bQA=/docs/5T9QALZ5bQABAAAAAAAAAA==/",
  "_etag": "\"0300aeae-0000-0000-0000-599081c20000\"",
  "_attachments": "attachments/",
  "_ts": 1502642619
}

3 実装

Visual Studio 2017 Preview(15.3)を起動します。

(1) プロジェクト作成

メニュー「ファイル → 新規作成 → プロジェクト」を選択します。

「新しいプロジェクト」ウィンドウが表示されます。
以下の入力を行います。

  • プロジェクトテンプレート:「Visual C# → Cloud → Azure Functions」
  • 名前:適当に・・・ここではデフォルトの FunctionApp1 にしました

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

設定用JSONのみの空っぽのソリューションが出来上がります。

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

(2) Functionsコードの追加

ソリューションエクスプローラで FunctionApp1 をマウス右ボタンクリック。
表示されたメニューから「追加 → 新しい項目」を選択します。

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

「新しい項目の追加」ウィンドウが表示されるので、「Azure Function」アイテムを追加します。
ここでは名前をデフォルトの「Function1.cs」としました。

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

「New Templates - Function1」というファンクションのトリガーを選択するウィンドウが表示されます。

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

「HttpTriggerWithParameters」を選択します。
また、「AccessRights」を「Anonumouse」としました。これにより、トリガーとなるHTTPリクエストを行う際に認証なしとなります(テストの利便性の為ここではそうしていますが、誰でもHTTPトリガーをキックできてしまうということなので、ご利用には気を付けてください)。
「FunctionName」はデフォルトの「HttpTriggerWithParametersCSharp」とします。

自動生成された Function1.cs 配下の通りです(リスト1)。

//リスト1 自動生成されたFunction1.cs

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

namespace FunctionApp1
{
  public static class Function1
  {
    [FunctionName("HttpTriggerWithParametersCSharp")]

    public static HttpResponseMessage Run(
      [HttpTrigger(
        AuthorizationLevel.Anonymous, 
        "get",
        "post", 
        Route = "HttpTriggerCSharp/name/{name}")]
      HttpRequestMessage req, 
      string name, 
      TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // Fetching the name from the path parameter in the request URL
      return req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
    }
  }
}

Run()メソッドがFunctionのエントリーポイントです。
第1引数「HttpRequestMessage req」に「[HttpTrigger]属性」が付与されています。HttpTrigger属性のパラメータで以下の設定が宣言されています。

(3) CosmosDB入力バインド実装の追加

まず、CosmosDB入力バインド用エクステンションを NuGetパッケージ より追加します。

ソリューションエクスプローラで FunctionApp1 をマウス右ボタンクリック。
表示されたメニューから「NuGet パッケージの管理」を選択します。

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

「NuGet パッケージの管理」画面が表示されたら、「参照」タブを選択し「Microsoft.Azure.WebJobs.Extensions.DocumentDB」を検索します。

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

Microsoft.Azure.WebJobs.Extensions.DocumentDB」を選択して、インストールボタンをクリックします。

では、 Function1.cs を開き、以下のように編集します。

// リスト2 CosmosDB入力バインド処理を追加したFunction1.cs

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

namespace FunctionApp1
{
  public static class Function1
  {
    [FunctionName("HttpTriggerWithParametersCSharp")]

    public static HttpResponseMessage Run(
      [HttpTrigger(
        AuthorizationLevel.Anonymous, 
        "get",
        "post", 
        Route = "HttpTriggerCSharp/name/{name}")]
      HttpRequestMessage req,
      [DocumentDB(
        "CompanyDB", 
        "EmployeeCollection",
        ConnectionStringSetting = "cosmosdb_DOCUMENTDB", 
        Id = "{name}")]
      dynamic document,
      string name, 
      TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // 入力ドキュメントをログ出力
      log.Info("input document info");
      log.Info(" firstname:" + document.firstname);
      log.Info(" lastname:" + document.lastname);

      // 入力ドキュメントに変更を加える
      document.age = 25;
      document.Salary = 15000000;

      // Fetching the name from the path parameter in the request URL
      return req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
    }
  }
}

Function1.csへの修正ポイントは以下の通りです。

  • 引数「document」の追加
    HttpTrigger引数に続けて「document引数」を追加しています。これが CosmosDB入力バインドパラメータになります。
    パラメータには「DocumentDB属性」を付与しています。
    入力バインドの情報として「データベース名」「コレクション名」を指定しています。
    「ConnectionStringSetting」は、Cosmos DBへの接続文字列情報です。接続文字列自体ではなく、接続文字列を設定した「構成情報キー」を指定します(設定方法は後述)。
    「Id」は、入力バインドするCosmos DBドキュメントの ID を表します。ここでは「{name}」としていますが、これはHttpTriggerのパラメータ「{name}」に該当します。「nameパラメータ値 = ドキュメントID」と、意味的に少し変ですが、自動生成コードの修正を最小限にしています。

  • documentのログ出力
    document引数には ID に該当するドキュメント オブジェクトが自動的にバインド設定されて呼び出されます。
    document.firstname / document.lastname をログ出力することで、内容を確認できるようにしています。

  • documentへのプロパティ値追加
    以下のようにdocumentに対して変更を加えています。
    入力バインドしたドキュメントへの変更は、Functionの正常終了後に Cosmos DBドキュメント に反映されます(データ更新が行われます)。

// 入力ドキュメントに変更を加える
document.age = 25;
document.Salary = 15000000;

(4) 公開 & Azureポータルでの構成

では、Azure上に公開します。

ソリューションエクスプローラで FunctionApp1 をマウス右ボタンクリック。
表示されたメニューから「公開」を選択します。

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

「新規作成」を選択して「発行」ボタンをクリックします(既にAzure FunctionsをAzure上に作成済みの場合には、「既存のものを選択」を利用します)。

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

Functionの名前等は適当に・・・ここでは Function Appの名前を InputBindExample として、「リソースグループ」「App Service プラン」「ストレージアカウント」は、それぞれ新規作成しました。

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

「公開」が成功した後の、Azureポータル画面が以下となります。
「Azure Functions(App Service)」「App Service Plan」「ストレージアカウント」の3つのリソースが作成されました。

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

Azure Functions「InputBindExample」を選択します。
「アプリケーション設定」をクリックして、Cosmos DBへの接続文字列項目を追加します。

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

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

(5) 実行

では、Functionを実行してみます。
まず、Azureポータルから実行します。

Function「HttpTriggerWithParametersCSharp」を選択します。
そして右側のテストタブを表示し、パラメータ「name」に「0000000001」を設定します。これは「入力パラメータname = ドキュメントID」として実装しているためです。
実行ボタンをクリックします。

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

出力されたログが以下の通りです。
ID = 0000000001 のドキュメント内容 firstname / lastname が出力されていることを確認することができます。つまり、確かにCosmos DBドキュメントが入力バインドパラメータとして引き渡されました。

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

次にAzureポータルで、Cosmos DBの「データエクスプローラ」を表示します。
age / salary 項目が追加されていることが確認できます。

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

匿名認証OK、HTTP GET許可としている為、ブラウザで「Https://inputbindexample.azurewebsites.net/api/HttpTriggerCSharp/name/0000000001」をアクセスする事でもFunctionをキックすることができます。

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

4 まとめ

ということで、Azure FunctionsへのCosmos DB入力バインドでした。
入力バインドといっても、Functionsの処理内でCosmos DBドキュメントに対して変更を加えることもできました。

Azure Functionsへの入出力バインドはCosmos DB以外にも「Blob storage」だったり、「Queue storage」だったりと、たくさんのバリエーションがあります。これらのバインドを利用すると、従来型の定型的ロジックの記述を省略することができます。
今回のCosmos DBバインドの例では、Cosmos DBへの接続処理の記述、取得・更新処理 の記述といったものが不要となりました。
つまり、煩雑なロジックの記述が Azure上のプラットフォームインフラ に吸収されていることにより、開発者はビジネスロジックの記述に注力することが可能になります。

本投稿では、ローカル実行やデバッグ等、色々端折っておりますが、Azure FunctionsへのCosmos DB入力バインドの一例として参考になればと思います。

「Serverless Meetup Tokyo #4」に参加した話

2017/8/9 「Serverless Meetup Tokyo #4」に参加してきました。

serverless.connpass.com

4回目の開催ということですが、私は初めての参加になりました。
すんごい人気なので、#5も今の時点で満員状態のようです(補欠当選も十分あるので、興味があれば、溢れてても登録しておいて損はないと思います。私も #4 では補欠繰り上げしたので^^)。

Azure勢 と AWS

で、私はこのブログを見てもわかる通り、いわゆる「Azure勢」です。
そして、この「Serverless Meetup Tokyo」は「AWS勢」が主軸です。

基本的には「Azure勢」とか「AWS勢」とか線引きをするべきではないし、したくないのですが、現在の多様化した技術世界においては、Azure / AWS の両方の技術をくまなく把握する人間というのは、ある意味限られた優秀な方々なのだと思っています。私のような凡人は、片方、もしくは両方に触手を伸ばしたとしても、どちらかに偏る事になるのだと思っています。
そして、一方に完全に偏ったとしても「Azureのすべてを知る」「AWSのすべてを知る」これ自体が非常に困難なほどの状況ではないかと思ってもいます。

楽しかったのですよ

そんな中での「Serverless Meetup Tokyo #4」だったのですが・・・

結論としては非常に楽しめました!

あのー、Azure系イベントに参加するよりも、もしかしたら(ある意味)楽しかったかも・・・これは、きちんと説明しないと語弊があるのだけれど・・・

私自身、Azureについては日々勉強を重ねています。
ですので、Azure系イベントでは、やはり「既知の領域」というものが大きくなってしまいます(あー、そんな偉そうなこと言える程ではないのだけれど・・・ご勘弁をm(_ _)m)。

しかし、AWSについては「完全な無知」ではないにしても「知らない領域」が広く存在しています。
ですから、自分自身にとって知らないことが多く、

  • 「へー、そうなんだー」

とか

  • 「このサービスはAzureでは何に該当するの?」

とか、はたまた、

  • 「あれ!?AWSだとこんな問題があるの?もしかしてAzureだと、この機能を使ったら解決出来てるんじゃないかなぁ」

とかいろいろ興味深いお話を聞くことができます。

結局・・・

そして、各セッションに対する感想はそれぞれあったのですが、今私はお酒も入っているのでざっくり、個人的総括で締めさせていただきます!

  • 「”サーバーレス アーキテクチャ”、全然 実践投入 いけるじゃん!」という自信を持てた
  • そうそう、やっぱり NoOps こそ目指すべき道だよ!人的リソースを運用メンテにかけるより顧客へのValueの提供にかけるべきだよ!

あと、個人的思い・・・

  • 20代のフレッシュエンジニアも、40代のおっさんエンジニア(&マネージャ)も楽しめて、より多くのValueを生成出来て、売上・実利を上げられる仕事がしたいよ

そして、今日のセッションでは得られていない、ビジネス的興味、そして整理したい点は以下。

  • ServerLessやPaaSを使うことのコストメリットについての整理

ではではー。
いつものブログとちょっと違うテイストになっちゃったかも・・・

【Cosmos DB】有効期限(TTL)で自動削除されるデータを作る

1 概要

Cosmos DBでは「有効期限付きデータ(ドキュメント)」というものを作ることができます。
ログ情報とかユーザーセッション情報とかのデータを ドンドコドンドコ 投入して、一定期間経過したデータは自動で削除するようなケースで利用可能です。

「よくあるファイル転送サービス(一定期間で削除される)」だったり「スナップチャット(的な)有効期限で自動削除されるサービス」だったり、そんなところでも使えるケースがあるかと思います。

ただし、ドキュメントが単独で自動削除される機能であるため、「一連の関連データを削除」というようなケースでは、単純に適用することはできないと思います。

1つ、特徴として「自動削除では RU は発生しない(RUは消費しない)」という点があります。

自前プログラムで、裏側でバッチ削除的なことをする場合、RUを消費するので、上手く使えばメリットのある機能になると思います。

1.1 設定レベル

有効期限の設定は、以下の2つのレベルに対して設定可能です。

  • 「コレクション」に対して設定する
  • 「ドキュメント」に対して設定する

1.1.1 「コレクション」に対して設定する

「Off / On(no default) / On」の3つの設定から選択します。

(1) Off

Off は、このコレクションにおけるドキュメントの有効期限自動削除を無効にします。ドキュメントに対して明示的に有効期限を設定しても、自動削除機能は働きません。

(2) On(no default)

On(no default) は、このコレクションにおけるドキュメントの有効期限自動削除を有効にします。既定の有効期限時間は設定しません。
有効期限削除を有効にしたいドキュメントに対して、明示的に時間を設定します。

(3) On

On は、このコレクションにおけるドキュメントの有効期限自動削除を有効にします。既定の有効期限時間も設定します。
既定の有効期限は、各ドキュメントでオーバーライドすることが出来ます。

また、有効期限は「秒」単位で設定可能です。

1.1.2 「ドキュメント」に対して設定する

ドキュメントに対して有効期間を設定する場合は、保存するドキュメントオブジェクトに対してJSONプロパティ名=「ttl」というint?型プロパティを追加します。

 [JsonProperty(PropertyName = "ttl", 
  NullValueHandling = NullValueHandling.Ignore)]
 public int? TimeToLive { get; set; }

※後述リスト3が完全版オブジェクト実装

1.1.3 「コレクション設定」「ドキュメント設定」の相関関係

コレクションに対する設定とドキュメントに対する設定の相関関係を表にまとめると以下の通りです。

コレクション設定 = Off コレクション設定 = On(no default) コレクション設定 = On(既定値 n秒)
ドキュメント設定 = なし 有効期限削除は無効(削除されません) 有効期限削除は無効(削除されません) 最終更新日時からn秒後に削除されます
ドキュメント設定 = -1 有効期限削除は無効(削除されません) 有効期限削除は無効(削除されません) 有効期限削除は無効(削除されません)
ドキュメント設定 = m 有効期限削除は無効(削除されません) 最終更新日時からm秒後に削除されます 最終更新日時からm秒後に削除されます

※「ドキュメント設定 = xx」と記述しましたが、コード上では int? 型として有効期限(TTL)を指定する事ができます。

1.2 削除タイミング

削除のタイミングは以下となります。

ドキュメントが最終更新されてから、
有効期限として設定された時間(秒)が経過したタイミング   

「最終更新日時(Last modified timestamp)」からの経過時間で自動削除判定が行われます。
「最終参照日時」ではないので、単純に ”一定期間利用されていないデータを削除する” といった用途には適用できないのでご注意を。

1.2.1 最終更新日時(Last modified timestamp)とは

最終更新日時(Last modified timestamp)とは、具体的には以下のものを指します。

ドキュメントの「_ts」値  

_ts値は、ドキュメントに対してCosmos DBシステムによって設定される値です。
ドキュメントの最終更新日時を表し、ドキュメントの作成時・ドキュメントの更新時に自動的に更新されます。
値は、「UNIX時間」であらわされます。

Azureポータルのデータエクスプローラでドキュメントを参照すると、以下のように _ts 値を確認することができます。

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

Fiddlerでドキュメント取得を行うRESTリクエストをキャプチャしても、以下のように _ts 値を確認することができます。

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

2 実装(コード)

では具体的な実装(コード)で「ドキュメントの有効期限設定」を行う方法を説明します。

2.1 コレクションに対して有効期限を設定する

まず「コレクションに対して」有効期限を設定する方法になります。

// リスト1 コレクションに対してドキュメントの有効期限を設定

private DocumentClient client = new DocumentClient(
  new Uri("https://cosmosdb.documents.azure.com:443/"), // あなたのURLを設定してね
  "【キー】"); // あなたのキーを設定してね

var database = 
  await client.CreateDatabaseIfNotExistsAsync(
    new Database() { Id = "ExampleDb" });
  
var collection = 
  await client.CreateDocumentCollectionIfNotExistsAsync(
    UriFactory.CreateDatabaseUri("ExampleDb"),
      new DocumentCollection()
      {
        Id = "ExampleCollection",
        DefaultTimeToLive = 60
      });

上記リスト1はデータベース(ExampleDb)とコレクション(ExampleCollection)を「(存在しなければ)新規作成」するロジックとなります。
そして、「DefaultTimeToLive = 60」の部分が「コレクションに対する、ドキュメント有効期限設定」になります。
つまり、最終更新日時から60秒でドキュメントが自動削除される設定となります。
「未設定 もしくは null」を設定すると、「Off」となります。

        DefaultTimeToLive = null

「-1」を設定すると、「On(no default)」となります。

        DefaultTimeToLive = -1

2.1.1 DocumentCollection.DefaultTimeToLive と 「Off / On(no default) / On」設定

コレクションに対する有効期限設定(DocumentCollection.DefaultTimeToLive)と「Off / On(no default) / On」の概念的設定の関連性は以下の通りとなります。

  • DocumentCollection.DefaultTimeToLive = n
    「On(no default)」に該当します。
    ドキュメントは、最終更新日時のn秒後に自動削除されます。

  • DocumentCollection.DefaultTimeToLive = null
    「Off」に該当します。

  • DocumentCollection.DefaultTimeToLive = -1
    「On(no default)」に該当します。
    ドキュメントの自動削除は有効ですが、自動削除時間はドキュメントの設定に依存します。

2.2 ドキュメントに対して有効期限を設定する

「個々のドキュメントに対して」有効期限を設定する例が以下のリスト2になります。

// リスト2 自動削除までの時間=100秒のドキュメントを作成

// コレクションに保存するオブジェクトを作成
UserAccount userAccount = 
  new UserAccount() { 
    UserID = "ryuichi111std", 
    UserName = "Ryuichi Daigo", 
    UpdateCount = 0, 
    TimeToLive=100 };

// オブジェクトをコレクションに保存(ドキュメントとして保存)
var document = await client.CreateDocumentAsync(
  UriFactory.CreateDocumentCollectionUri("ExampleDb", "ExampleCollection"),
  userAccount);

Cosmos DB(DocumentDB)に保存するデータ型「UserAccount」が唐突に登場していますが、これは以下のリスト3の型となります。
「TimeToLiveプロパティ」値が有効期限(TTL)となります(ドキュメントの有効期限は100秒)。
リスト3にあるように「jsonプロパティ名 = ttl」とします。これがドキュメントの有効期限を示すプロパティとなります。

// リスト3 保存するドキュメントの型はこれ

using System;
using Newtonsoft.Json;

namespace AutoExpireDataExample
{
  public class UserAccount
  {
    [JsonProperty(PropertyName = "id")]

    public string UserID { get; set; }

    public string UserName { get; set; }

    public int UpdateCount { get; set; }

    [JsonProperty(PropertyName = "ttl", NullValueHandling = NullValueHandling.Ignore)]
    public int? TimeToLive { get; set; }
  }
}

3 まとめ

ということで「有効期限で自動削除されるデータ(ドキュメント)」についてのエントリーでした。
ドキュメントの削除処理に関したは「単に特定コレクションドキュメントが単独で消えていい例」それから「プログラマティックに関連データも同時にカスタム実行により削除したい例」とあると思います。
その上で、RU消費のない「ドキュメントの有効期限自動削除」は魅力的な機能でもあります。
使いどころの難しさはありますが、うまく活用すれば有益な機能の1つであると思います。

Azure FunctionsからCosmos DBに出力バインドする(2)~.csコンパイル編

1. はじめに

前回のエントリーに引き続きの投稿になります。
Azure Functions 出力バインドを利用し Cosmos DB にデータ出力を行います。

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

前回はAzureポータルのみでFunctionsの作成を行いました。
また、ソースコードの実装は .csx(C#スクリプト) をポータル上で編集しました。

今回は .csファイル で実装を行い、コンパイルアセンブリをAzure Functionsに発行することにします。
.csxでの実装に比べ、実行時コンパイル処理が省かれるので、起動時の動作が早くなります。

本エントリーのみでも理解できる内容として記述しますが、前回のエントリーと重複する部分は手短に記述します。ということで、前回のエントリーを見ていただけるとより理解しやすいかと思います。

ryuichi111std.hatenablog.com

2. 開発環境

コンパイル実装をするため、本エントリーでは以下の環境を利用します。

※ 2017/7/20現在、Stableな Visual Studio 2017 では 「Function Tools for Visual Studio 2017」がサポートされていません。

3. 前提条件

以下のAzure環境を前提条件とします。

3.1 Cosmos DB アカウント

以下のような 出力先 Cosmos DB アカウント を用意している前提とします。

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

API(データモデル)は「SQL (DocumentDB)」です。

3.2 Function App

前回エントリーで作成した以下の「Function App」を用意している前提とします。

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

「アプリ名」:RdExampleFunctionApp
「リソースグループ」:RdExampleFunctionApp
ホスティングプラン」:従量課金プラン
「場所」:西日本
「Storage」:新規作成 rdexamplefunctionapp

4. ② VS2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装

VSを使用した実装に移りますが、「4.1 基本的なAzure Functionsプロジェクトの作成から発行まで」と、その上に「4.2 osmos DB出力バインドの追加からパブリッシュまで」の2段階で進めたいと思います。

4.1 プロジェクトの作成から基本パブリッシュまで

まず第1段階として、基本的なAzure Functionsプロジェクトの作成から発行までを行います。

(1) Visual Studio 2017 Preview(15.3)の起動

Visual Studio 2017 Preview(15.3)を起動します。
「Azure Function Tools for Visual Studio 2017」のインストールを済ませておいてください。

(2) Azure Functionsプロジェクトの作成

「ファイル → 新規作成 → プロジェクト」を選択します。
「新しいプロジェクト」ウィンドウで、テンプレートカテゴリーから「Cloud」を選択、プロジェクトテンプレートとして「Azure Functions」を選択します。
プロジェクト名は、ここでは「CosmosDbBindExampleFunction」としました。

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

以下のようなプロジェクトが作成されます。

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

(3) Functionソース(.cs)の追加

新規作成したプロジェクトには、まだ1つもFunction実装がありません。
Functionの実装ソース(.cs)を追加します。
ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「追加 → 新しい項目」を選択します。

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

表示された「新しい項目の追加」ウィンドウから「Azure Function」を選択し、名前を入力します。ここではデフォルトのまま「Function1.cs」としました。

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

「New Azure Function」ウィンドウが表示されます。

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

ここでは、設定内容は以下とします。

  • トリガーの種類: HttpTrigger
  • AccessRights: Anonymous(今回は簡易に匿名で利用できるようにします)
  • FunctionName: HttpTriggerCSharp

自動生成された Function1.cs は以下の通りです。

// 自動生成された Function1.cs 

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 CosmosDbBindExampleFunction
{
  public static class Function1
  {
    [FunctionName("HttpTriggerCSharp")]
    public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]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);
    }
  }
}

(4) Nugetパッケージの更新

自動生成されたプロジェクトおよびfunction1.csは、2017/7/20現在の確認では、そのままではビルドが通りません。
Nugetパッケージの更新を行う必要があります。
ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「NuGetパッケージの管理」を選択します。

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

「更新プログラム」を選択、「プレリリースを含める」にチェック、「すべてのパッケージを選択」にチェックし、「更新」ボタンをクリックします。

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

(5) ビルド&発行

ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「発行」を選択します。

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

発行のUIが表示されるので「既存のものを選択」を選択して「発行」ボタンをクリックします(今回はAzureポータル上に既に RdExampleFunctionApp というFunction Appを作成済みの為)。

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

発行対象のFunction Appである「RdExampleFunctionApp」を選択して、「OK」ボタンをクリックします。

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

発行が完了したらAzureポータルでFunctionを確認します。
HttpTriggerCSharp関数が発行されて事を確認することができます。

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

4.2 Cosmos DB出力バインドの追加からパブリッシュまで

次に第2段階として、Cosmos DB出力バインドの追加を行います。

(1) DocumentDB Extensionの追加

Cosmos DBへの出力バインドを実装するために、以下のNuGetパッケージを追加します。

  • Microsoft.Azure.WebJobs.Extensions.DocumentDB

NuGetパッケージの管理画面から「Microsoft.Azure.WebJobs.Extensions.DocumentDB」を検索&選択して「インストール」を行います。

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

(2) Function1.csの実装を修正

Function実装を Cosmos DB出力バインド を行うように修正します。

// Cosmos DB出力バインド実装を加えた Function1.cs

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

namespace FunctionApp2
{
  public static class Function2
  {
    [FunctionName("HttpTriggerCSharp")]
    public static HttpResponseMessage Run(
      [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
      HttpRequestMessage req,
      [DocumentDB("CommunicationDB", "MessageCol",
            CreateIfNotExists = true,
            ConnectionStringSetting = "cosmosdb_DOCUMENTDB")]
      out object messageDocument,
      TraceWriter log)
    {
      string yourName = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "yourName", true) == 0)
        .Value;

      string message = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "message", true) == 0)
        .Value;

      // 出力パラメータmessageDocumentに設定された値がCosmos DBに出力される
      messageDocument = new
      {
        yourName = yourName,
        message = message
      };

      if (!string.IsNullOrEmpty(yourName) && !string.IsNullOrEmpty(message))
      {
        return req.CreateResponse(HttpStatusCode.OK);
      }
      else
      {
        return req.CreateResponse(HttpStatusCode.BadRequest);
      }
    }
  }
}

上記実装のうち重要なポイントを2つ以下に記述します。

①メインの Run() メソッドの引数

前回の.csxファイル形式での実装では、引数のバインドをAzureポータルのGUI上で行いました。
AzureポータルGUI上での操作は、実際には function.json ファイルに反映されます。つまり、GUI操作はエディタとしての機能であり、本質的には function.json への記述で設定を行いました。
コンパイルベースのFunction定義では、メソッド引数への属性設定により同様の設定を行います(つまり、引数属性設定により入出力バインドの設定を行うことができます)。

  • 第1引数

    [HttpTrigger(AuthorizationLevel.Anonymous, “get”, “post”, Route = null)] HttpRequestMessage req

入力のHttpTrigger定義となります。匿名ユーザーのアクセスを許可し、GET / POST を受け入れます。ルーティング定義はnullとしています。

  • 第2引数

    [DocumentDB(“CommunicationDB”, “MessageCol”, CreateIfNotExists = true, ConnectionStringSetting = “cosmosdb_DOCUMENTDB”)] out object messageDocument

こちらがCosmos DB出力バインドの定義になります。
CommunicationDBデータベース、MessageColコレクションへの出力としています。
CreateIfNotExists=trueは、本Functionの実行時にCosmos DB側のデータベース・コレクションが存在しなかった場合に自動生成するかどうかの設定です。ここでは、trueなので存在しなかったら作成する意味となります。
ConnectionStringSetting 値は、出力先のCosmos DBへの接続文字列を表します。ただし、接続文字列自体ではなく、接続文字列を設定したアプリケーション設定キーとなります。
アプリケーション設定キーは、Azureポータルで設定・確認することができます。
RdExampleFunctionAppを選択し、「アプリケーション設定」をクリックします。

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

「アプリ設定」項目に「キーと値」の設定が行えるので、ここに「cosmosdb_DOCUMENTDB」キーを追加し、値としてCosmos DBアカウントへの接続文字列を追加します(前回の.csx形式によるブログエントリーを実施している場合、既に当該キーは追加されていると思います)。

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

②Cosmos DB ドキュメントの作成

出力バインド定義が行われた引数 messageDocument をnewで生成することで、作成するCosmos DB(DocumentDB)ドキュメントを指定することができます。

messageDocument = new
{
yourName = yourName,
message = message
};

(3) 発行

VSから発行を行います。
※ 既に前述で発行方法は説明したので、ここでは割愛します。

(4) テスト実行

今回発行したFunctionはHttpTriggerで起動します。
匿名ユーザーによるリクエストも許可している為、以下のURLによりキックすることができます。

https://rdexamplefunctionapp.azurewebsites.net/api/HttpTriggerCSharp?yourName=ryuichi.daigo&message=hello azure functions

ブラウザで上記URLをリクエストします。

AzureポータルからCosmos DBのデータエクスプローラを確認すると、以下のようにドキュメントが作成されたことを確認することができます。

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

5 まとめ

Azure Functionsは、超基本でいうとあくまで「Function(関数)」なのですが、今回紹介したBindを含め、やはり色々な機能を持っています。最近ではDurable Functionsなんてのも出てきましたし。
そんな進化し続けるAzure Functionsですが、Visual StudioのToolkitのリリースはやや遅めな印象を持っています。とはいえ、そろそろ正式版も出るんじゃないかなあ・・・とも思うので、そのあたりの開発インフラ事情が更新されたら、本エントリーも改めて修正していきたいと思います。

Azure FunctionsからCosmos DBに出力バインドする(1)~.csx編

Azure Functionsには、その実行に関して「トリガー」「バインド」という概念が存在します。

  • 「トリガー」
    ファンクションの実行のきっかけとなるもの。「1つのファンクション」に「1つのトリガー」が定義されます。
  • 「バインド」
    そのファンクションで処理する入力方向および出力方向のデータを表します。

※Azure Functionsのトリガーとバインドの本質的な解説は以下のあたりを参考に・・・

docs.microsoft.com

1. 今回の実装概要

今回はAzure Functionsの出力バインドとして Cosmos DB を利用する方法についてまとめます。

実装するものは以下のようなイメージになります。

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

まず、用意する Azure Functions は、HTTPリクエストをトリガーとして実行されます。
FunctionsはCosmos DBを出力バインドとします。

つまり、Azure FunctionsをホストするURLに対してHTTPリクエストが行われると、Functionが実行され、結果としてCosmos DBに対してドキュメントが作成される(出力される)、という動きになります。

前提条件

今回は出力バインドとして Cosmos DB を用います。
ということで、前提条件として以下のような Cosmos DB アカウントを用意していることを前提とします。

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

API(データモデル)は「SQL (DocumentDB)」です。

2. 2つの実装方式

Azure Functionsは実運用にも耐えうる状態と考えられますが、開発ツールの統合という意味ではまだまだ過渡期的な状況です(2017/7/19現在)。
今回は以下の2つの方法でAzure Functionsを実装します。

  • ① Azureポータルのみを使ったcsxによる実装
    Azureポータル上の操作のみで実装します。実装ファイルはcsx(C#Script)で行います。

  • Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装
    実装はcsで行います。つまりコンパイルイメージをAzure Functionsにアップします。

共通の初期設定

まず、「1 Azureポータルのみを使ったcsxによる実装」「2 Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装」の両方に共通の設定を行います。

「Function App」の作成

Azureポータルにログインします。

(1) Function Appの検索

「+新規」をクリックし、検索ボックスに「Function App」と入力します。
候補一覧に表示された「Function App」を選択します。

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

(2) 作成

「作成」ボタンをクリックします。

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

(3) パラメータの入力

アプリ名等々を適時入力して「作成」ボタンをクリックします。
ここでは以下の内容としました。
「アプリ名」:RdExampleFunctionApp
「リソースグループ」:RdExampleFunctionApp
ホスティングプラン」:従量課金プラン
「場所」:西日本
「Storage」:新規作成 rdexamplefunctionapp

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

(4) 作成されたリソースの確認

(3)の後、しばらく待っているとAzure Functionsを構成するリソースが作成されます。
作成したリソースグループ「RdExampleFunctionApp」は以下となります。

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

「App Serviceプラン」「ストレージ アカウント」「App Service」の3つのリソースが作成されました。
Azure Functionsは基本的にApp Serviceであり、また、その動作の際にはAzure Storageを利用します。その為、上記のような3つのリソースが生成されました。

3. 「① Azureポータルのみを使った.csxによる実装」

では、Functionsの作成に移ります。

3.1 Function Appの選択

リソースグループから(もしくはリソースの一覧から)「RdExampleFunctionApp」を選択します。

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

3.2 関数の追加

関数の「+」マークをクリックし、右側のペインから「カスタム関数を作成する」をクリックします。

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

カスタム関数作成画面が表示されます。

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

以下の作成パラメータを入力します。
テンプレート:「HttpTrigger - C#
関数名の指定:「HttpTrigger1」
承認レベル:「anonymous」 ※認証なしのHTTPリクエストを受け入れるトリガーとします。

「作成」ボタンをクリックすると関数(Azure Function)が作成されます。

3.3 関数の実行

自動生成された HttpTrigger1 ファンクションを実行してみます。

実行ボタンをクリックします。

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

画面下側の「ログ」ぺインに、コード「log.Info(…)」で実装されたログ文字列が表示されるのを確認することができます。
ひとまず、デフォルトで生成されるファンクションの実行をすることができました。

3.4 Cosmos DB出力バインドの設定

次にCosmos DB出力バインドの設定を行います。
左側のファンクション メニューから「統合」を選択します。

(1) 出力の作成

更に、右側に表示された設定画面から、出力→「+新しい出力」をクリックします。

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

(2) DocumentDB形式の選択

一覧から「Azure DocumentDB ドキュメント」を選択し「選択」ボタンをクリックします。

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

(3) 出力バインドの詳細設定

Azure DocumentDB(Cosmos DB)出力バインド設定を行います。
設定項目は以下の通りです。

  • ドキュメントパラメーター名:ファンクションの引数名。この名前で受け取った引数(変数)でCosmos DBのドキュメントを操作します。
  • データベース名:操作対象のデータベース名を設定します。
  • コレクション名:操作対象のコレクション名を設定します。
  • DocumentDBのデータベースとコレクションが作成されるようにしますか?:データベース・コレクションが未作成状態の時に、自動生成するかどうかのフラグを設定します。

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

続けて「DocumentDBアカウント接続」→「新規」をクリックします。

以下の画面のように、Cosmos DBアカウント一覧が表示されるので、操作対象のCosmos DBアカウント「cosmosdb」を選択します。

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

以上で、Cosmos DBへの出力バインドの設定が完了しました。
以下が設定完了後の画面です。

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


※2017/7/20追記
上記、Azureポータル上でのバインド設定は結果として function.json に反映されます(以下の画面)。

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

AzureポータルGUI操作によるバインド設定は、function.jsonを編集する為のエディタであり、本質的な設定記述場所は function.json となります。


3.5 関数(ファンクション)実装の修正

次に、関数(ファンクション)の実装を修正します。
左側のメニュー(?)から「HttpTrigger1」を選択し、run.csxのエディタ画面を表示します。

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

実装を以下に修正して、「保存」ボタンをクリックします。

using System.Net;

public static HttpResponseMessage Run(HttpRequestMessage req, out object outputDocument, TraceWriter log)
{
  string yourName = req.GetQueryNameValuePairs()
    .FirstOrDefault(q => string.Compare(q.Key, "yourName", true) == 0)
    .Value;

  string message = req.GetQueryNameValuePairs()
    .FirstOrDefault(q => string.Compare(q.Key, "message", true) == 0)
    .Value;

  outputDocument = new {
    yourName = yourName,
    message = message
  };


  if (!string.IsNullOrEmpty(yourName) && !string.IsNullOrEmpty(message)) {
    return req.CreateResponse(HttpStatusCode.OK);
  }
  else {
    return req.CreateResponse(HttpStatusCode.BadRequest);
  }
}

実装内容のポイントは、以下の通り。

  • クエリーパラメータとして「yourName」「message」の2つのパラメータを文字列で受けとります。
  • 受け取ったパラメータからCosmos DBのドキュメントを生成します。
  • 引数「outputDocument」は、出力バインド設定「1.4 (3)」の「ドキュメントパラメーター名」と一致している必要があります。

3.6 テスト実行

右側の「テスト」をクリックし、HTTPメソッドを「GET」に設定します。
「クエリ」には「パラメータの追加」で「yourName」および「message」を追加します。
これらは既に実装した関数(ファンクション)の仕様に合わせた呼び出し方法となります。
「実行」ボタンをクリックします。

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

3.7 テスト結果の確認

AzureポータルでCosmos DB(cosmosdbアカウント)の「データエクスプローラ」を表示します。
以下の画面のように ドキュメント が生成されていることを確認することができます。

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

4. まとめ(&つづく・・・)

ということで、思ったより長くなったので「(2) Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装」は、あらためエントリーを分けて投稿したいと思いますm(_ _)m。

おまけ

AzureポータルGUIで設定した Cosmos DB出力バインド による接続文字列設定は、以下の場所に保存されています。
作成した Azure Functions「RdEzampleFunctionApp」を選択し、「アプリケーション設定」を選択します。

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

「アプリ設定」の下に構成設定として設定値が保存されています。

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