BEACHSIDE BLOG

Azure と GitHub と C# が好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

Form Recognizer の Form OCR Testing Tool のセットアップ方法 (2021年3月 version)

Azure の Cognitive Services の中のひとつ、Form Recognizer をサクッと試せるツール Form OCR Testing Tool のセットアップ方法のメモです。

実際に使ってどれくらいの精度でるんやろってのがみたいところですが、それは分析した請求書といったフォームへの依存が強い可能性もあるので触れません。自分が持ってる請求書とかをいくつか試したところだとかなりいい感じではありました。

今回セットアップ方法を試すのは、2021年3月15日に公開された v2.1-preview.3 を使うバージョンになります。

流れはこんな感じです。

  • Azure で必要なリソースを作成
  • 公開されている Form OCR Testing Tool の Web に接続して設定をする

Azure のリソース作成

Azure ポータルでこの2つを作成します。

  • Form Recognizer のインスタンス(ようはキー)
  • Blob ストレージ

Form Recognizer の作成とキー情報の取得

Form Recognizer を利用するには、Azure portal から Form Recognizer のリソース...要はキーとかを作成する必要がありますので作っていきましょう。

Azure portal を開いてリソースの新規作成で 「form」とか入力すると Form Recognizer が出てきますので作成を進めます。

f:id:beachside:20210310205935p:plain:w480

作成の画面では。名前と料金プランを選ぶだけなので特に迷うところはありません。
Free プランもありますが今回は S0 を選んでます。

f:id:beachside:20210310210035p:plain:w600

作成の画面では AI 系サービスあるあるの利用規約がでて create ボタンをクリックすると同意したことになります。

f:id:beachside:20210310210220p:plain:w600

作成が完了したら、作成したリソースに移動してみましょう。Keys and Endpoint のメニューに、キーとエンドポイントの情報などがあります。あとで使うのでメモしておきます。

f:id:beachside:20210310210846p:plain

Blob の作成

Custom Form の API を使って、請求書のなどのフォームに自身でラベル付けをして機械学習のモデルを作る場合に必要になります。

Prebuilt のモデルを使ったり Layout API を使うだけであれば Blob は必要ないので、この準備は不要です。

Blob の Storage アカウントを作成するのはポチポチやるだけなので書くのは省略します。Storage アカウントを作成後にやる2点を書いていきます。

1つめ: CORS の設定

作成した Storage アカウントで CORS のメニューをクリックし (図①) 、 Blob Service のタブで CORS の設定をします (図②) 。設定内容は以下です。設定後、画面上部の Save ボタンをおして保存します (図③) 。

  • Allowed origins = *
  • Allowed methods = すべてのメソッドを選択
  • Allowed headers = *
  • Exposed headers = *
  • Max age = 200

f:id:beachside:20210316144603p:plain

2つめ: SAS の取得

Form OCR Testing Tool からセキュアに Blob のコンテナーへアクセスするために SAS (Shared Access signature) を生成します。

まず、Form Recognizer に使うコンテナーを作成していない場合は、Storage アカウントのメニューで Container をクリックし (図①)、コンテナーの作成ボタンをクリック (図②) して作成します。
図③のように Name は適宜入力、Public access level は Private で大丈夫です。最後に Create のボタンをクリックして作成します。

f:id:beachside:20210316145607p:plain

作成した Container の右側の "・・・" をクリック > Generate SAS をクリックします。

f:id:beachside:20210316150717p:plain

SAS を生成する際のポイントは以下くらいでしょうか。ほかの部分はデフォルトの設定値で大丈夫です。

  • Permission を "Read", "Write", "Delete", "List" (日本語では "読み取り" "書き込み" "削除" "リスト") の4つにチェックをつける
  • 有効期限の日時を必要に応じて設定する

f:id:beachside:20210316151443p:plain:w400

Generate SAS token and URL をクリックすると SAS が生成されます。Blob SAS URL の値を後ほど使うのでメモしておきましょう。

f:id:beachside:20210316151658p:plain:w400

Form OCR Testing Tool のセットアップ

オープンソースである Form OCR Testing Tool はローカル環境で動かしたり Docker の環境で動かしたりできます。今回は一番手軽な方法として、公開されてる Web のサービスを使います。

v2.1 preview3 の最新機能を使いたいので、以下の URL にアクセスします。

https://fott-preview.azurewebsites.net/

あくまでこのブログを書いた時点の情報なのでアクセス後に画面右下に表示される API のバージョンが使いたいバージョンなのか確認しましょう。v2.1 preview3 から始まっていれば問題ありません。

f:id:beachside:20210316044444p:plain

Prebuilt の API の試す

画面左側の Prebuilt analyze のアイコンをクリックすると画面が表示されます。
分析したいファイルのパスを指定して、Form Recognizer の エンドポイントの URI と API key を入力すれば利用できます。

f:id:beachside:20210316060738p:plain

Prebuilt の API は、請求書、レシート、名刺、IDカードの4つモデルが用意されていて、汎用的に使えるものです。

そして、残念なことにこれは日本語に対応してません。

ただ、v2.1-preview.3 で新しく公開された Form type が ID のモデル (パスポートやアメリカの運転免許証から情報を取得できるモデル) では、日本のパスポートがいい感じに認識できました。国によってレイアウトが異なるのパスポートの OCR での情報取得はめんどいなーと思う経験をしたことがあったので、レイアウトに依存せずサクッと情報が取得できるならいいですね。

(注: 私は日本のパスポートしか試してないので、アメリカ・カナダのパスポートもきっと大丈夫として、他の国のパスポートがいい感じに解析できるかは不明ですがね)

Layout API を試す

日本語対応している API です。

請求書からデータを抽出して JSON または CSV で結果を出力できます。請求書とかはまずはこの API を試してみるのがいいです。画面左側の Layout analyze を開いて、 Form Recognizer の endpoint の URI と API を入力すれば利用できます。フォームは URL またはローカルファイルのパスを指定します。

自分のガチの請求書はほぼ完ぺきに認識できました。ネットに落ちてる適当なサンプルの請求書を分析してみた中でこれが一番精度が悪くテーブルの読み込みがいまいちでしたが、それでも合計金額とか文字の認識はちゃんとできてました。

f:id:beachside:20210316064851p:plain

Custom Form を試す

これも日本語に対応している API です。

独自のラベル付けをして機械学習させることでより複雑なフォームを正しく認識させることができる API です。Layout API の精度がいまいちだったら試すって流れと思っています。

機械学習といっても5枚程度のフォームにラベルをつけてボタンをポチっとするだけなので、Custom Vision 並みに気軽にできます。

Custom Form を利用するには Connection とプロジェクトを作成する必要があるので、そのセットアップをメモしておきます。

Connections の作成

Connectionの作成、具体的には Blob storage の接続を登録することです。この Blob にこの Custom にモデルをトレーニングするプロジェクト自体の管理ファイルの保存したり、分析するソースのファイルを置いたりで使います。

手順は:

  • 左側の Connections アイコン(図①)をクリックして New Connection のアイコン(図②)をクリック
  • 先ほど作成した Blob の情報を入力します(図③)。注意点としては先ほど作成した SAS URI を正しく入力くらいでしょうか。Display name や Description は適当に入力します。Provider は今のとこ Azure blob container しか選べないです。
  • Save Connection をクリックすると Connection が保存されます。

f:id:beachside:20210316054847p:plain

プロジェクトの作成

左側の Home のアイコンをクリック > "Use Custom to train..." をクリックします。

f:id:beachside:20210316155424p:plain

New Project をクリックします。
(ちなみにOpen Cloud Project は保存されたプロジェクトを開くときに使うものです。)

f:id:beachside:20210316155732p:plain

プロジェクトの設定画面が表示されます。ここで Connection を選択したり Form Recognizer の URI や API key を入力して Save Project をクリックするとプロジェクトができあがります。

ちなみに Folder path は、Blob container の中でソースのファイルを置くサブフォルダを指定したい場合に入力します。

作成すると Blob container に .fott という拡張子のプロジェクトファイルが生成されます。これで Custom Form を実行する準備は整いました。

f:id:beachside:20210316155831p:plain

作成後 Tag Editor (図①) が表示されます。

これで Blob にファイルをアップロードすると、自動で Tag Editor にファイルが読み込まれプレビューされます。この後の流れは。まずは5つのフォームをアップロードしてタグをつけて Train (図②) にてトレーニングを実行し、テストデータを使って精度をテストすることです。
今回はセットアップ方法のブログなので Train > test のところは書きませんが、公式ドキュメントだとここら辺です。

f:id:beachside:20210316160658p:plain

Form OCR Testing Tool のプロジェクトを他のユーザーとの共有

ちょっと余談になりますが、Custom にトレーニングしたプロジェクトは、クレデンシャルを保存することでをそれを用いて他のメンバーと共有することができます。方法は、GitHub の READMEに記載されているのでここで細かくのはやめときます。

日本語の README を作成して Pull Request を出したのでそのうちマージされたら日本語でも見れるようになると思います。

公式の README 自体があまり更新されてないのでちょっと UI とか古いってのはありますが...

終わりに

2021年3月15日に英語のドキュメントが更新されたので、日本語のドキュメントは本日時点だとまだ古いです。日本語ドキュメントを見るときは英語のドキュメントの更新日付と見比べて同じになってるかに注意しましょう。古ければ英語のを見た方が良いです。

参考

github.com

docs.microsoft.com

Azure Cognitive Search - データをインポートしてインデックス作成する

全開のブログではインデックスがある前提でインデックスを push モデルで更新する方法と、camel case への変換に関する Tips を書きました。

blog.beachside.dev

インデックスありきの話を書いたので、今回はそもそものインデックスを作るところのメモです。
具体的には、データが Azure の Blob や Cosmos DB にあってそれをインポートしてインデックスを作ろうって話です。

ハマりポイントが一点あり、それをメモしておきたかった次第です。

事前準備

インポートするのは以下のようなデータにしてます。データは、string と bool と int の型混在、あとはネストで値を持ってるデータを突っ込もうと思った程度でそれ以外は特に意味がない適当です。

[
    {
        "id": "73a0ab73-64a7-4fe2-bd41-f2c4d05fabbb",
        "person": {
            "id": "cde2d9dc-0d47-4320-85d7-46e8e5a2a5a1",
            "firstName": "Noah",
            "lastName": "initial-data"
        },
        "level": 22,
        "isWizard": true
    },
    {
        "id": "0ae80271-6a95-4ac9-8aa6-73a27b3463ab",
        "person": {
            "id": "f6402297-e1df-400c-b835-74450250d93a",
            "firstName": "John",
            "lastName": "initial-data"
        },
        "level": 69,
        "isWizard": true
    },
    {
        "id": "73e2a2bc-1c4b-440c-9fc9-045031d35d9e",
        "person": {
            "id": "2b04c169-a7ec-402d-82a6-9f6f3fb3d28b",
            "firstName": "Taro",
            "lastName": "initial-data"
        },
        "level": 63,
        "isWizard": true
    }
]

これを Blob のリソースを作っておいておきましょう(手順は省略)。
Blob の Container の パブリックアクセスレベルは プライベート で OK です。Cognitive Search がちゃんと接続してくれます。

f:id:beachside:20210226205238p:plain

※ 今回 Blob でやるのは、Blob でハマりポイントがあるからです。

インデックスの作成

SDK を 使って C# などでインデックスを作成することも可能ですが、インデックスのスキーマの設定を C# のコードで表現すると Attribute がごちゃごちゃとつくので、そのクラスを他のドメインと共有するならその部分が嫌な感じがします。

プロダクションの運用のために IaC 化するとかだとコード化できることが正義ですが、そうでない場合(調査とか検証用にサクッと作りたいとか)だとめんどいだけです。

そんなときは Azure Portal から GUI で既存のデータソースからデータをインポートして作成するのがお手軽なので、それをやっていきましょう。

Azure Portal から データをインポートしてインデックスを作成

Azure Portal で Cognitive Search のリソースを作ったら、概要 にある データのインポート をクリックします。

f:id:beachside:20210226204410p:plain

データソースで Azure BLOB ストレージ を選択します。ほかにインポートできるデータソースはここで選べるものです。ちなみにサンプルを選んで MS が用意してるサンプルデータからインデックスを作成することもできます。

f:id:beachside:20210226210856p:plain

データに接続します のタブ(翻訳が変...)では、先ほどデータを置いた Blob を指定しました。ほかの項目はよしなに設定です。

f:id:beachside:20210226211419p:plain

次は Cognitive Search の特徴的な機能で AI との連携に関するところですが、今回は使わないのでスルーして次に進みます。

f:id:beachside:20210226214512p:plain

次が、インデックスをどう作るかの部分です。

  • まずはインデックス名をそれっぽくしましょう(図①)。
  • キーはBlob から作成すると、key は AzureSearch_DocumentKey という謎の値がセットされます (図②)。例えば Json のデータの id を キーにしたい場合は変更しましょう。
  • Blob からデータをインポートすると Blob のメタデータも読み込んでくれます。不要な場合は削除します(図③)。

最後に書くフィールドに対して属性を定義します(図④)。属性でぱっとみわかりにくいのが フィルター可能検索可能 です。フィルター可能はざっくり説明すると、対象の値を完全一致で検索する感じです。 検索可能フルテキスト検索をするかという意味です。そのためこれにチェックを入れるとパーサーを選択する必要があります。よくわからんという方は 日本語 - Lucene を選択して動作を確認するで問題ないです。

詳しくは公式ドキュメントを確認しましょう。

f:id:beachside:20210226215533p:plain

最後に インデクサーの作成 タブです。ここはサクッと 送信( Submit の翻訳が変...) のボタンをクリックしそうですがあかんです。まずはインデックスの名前をいい感じの名称に変えましょう。

そしてここが今回書きたかったポイントですが、Blob からインポートしたときは 詳細オプション を開いて Base-64 エンコード キー を見ましょう。このチェックがデフォルトでついているので、さきほど指定したキーがエンコードされてしまいます。キー に指定した値をそのまま使いたい場合は チェックを外します

ちなみに Cosmos DB からインポートするときはこのチェックはデフォルトで外れています。

f:id:beachside:20210226221218p:plain

これで無事にインデックスが作成されます。

動作確認

インデックスが作成出来たら、概要 > インデックスのタブを開いて、作成したインデックスをクリックすると検索をお試しできます。

f:id:beachside:20210226221615p:plain

まとめ

キーの値が意図したものと違ったら Base64 encode されてるので注意!ってとこです。

Azure Cognitive Search - 検索のインデックス更新の基礎 ( C#, SDK v11 )

Azure Cognitive Search でインデックスを更新する際 C# でサクッと更新できるのですが、それに関するメモです。

インデックスの更新は大きく2パターン

Pull model

データソースを設定しておくことで、Cognitive Search が (正確にはインデクサーが)データソースからデータをプルしてインデックスの追加・更新を行う方法です。定期的に実行する機能もついています。

インデックスを最初に作るときや、とりあえずさっと試してみたいときは、この方法でさっと作るのがよいです。

データソースは以下が対応してます。Blob に json のデータを置いてプルするとかめっちゃ簡単にできます。ほかのデータソースでも簡単。

  • Blob Storage (Data lake Storage Gen2 は preview)
  • Table Storage
  • Cosmos DB
  • SQL Database, SQL Managed Instance, Azure VM 上の SQL Server

Push model

データソースに変更があったタイミングでそのデータを Cognitive Search へ Push する方法です。
ほぼリアルタイムにインデックスを更新できるので、プロダクションの運用では、多くの場合この方法が有効だと思っています。

例えばデータソースが Cosmos DB だと Change Feed でインデックスを更新できるので相性が良いです。

注意点として1度に送信できるのは 1000個 or 16MB という制限がありますが、分割して送信すればよいだけなので問題になることはないと感じています。

SDK v11 (C#) の基礎知識

これからは基本的に古いバージョンは使わず v11 以降を使うと思うので、それを前提にいくつか基本的な情報を整理しておきます。

パッケージ名

v10は Microsoft.Azure.Search というパッケージでしたが、v11 では Azure.Search.Documents に変わりました。class 名もかなり変わっているので間違って v10 のドキュメントをみないよう注意しましょう。

Json の Serializer

v11 の Json の処理は、System.Text.Json に依存しています。
たまにやりそうな Attribute で Json のプロパティ名を定義している場合は多少の注意が必要になります。具体的には、Json.NET の [JsonProperty("...")] だと変換されません。System.Text.Json の `[JsonPropertyName("...")] を使う必要があります。そのため、Json.NET に依存してるライブラリと v11 SDK を併用してるとめんどくさってなります。
そんなときは class に attribute を付けて管理するのではなく、SearchClient のインスタンス化時に Serializer を設定して上がるパターンがベターです。実装方法は後述します。

実装してみる

Azure Portal から必要な情報を取得

今回は SDK の使い方のメモなので、Cognitive Search を作ってインデックスが作られてることを前提に進めます。今回 SDK を使う際に必要なのは3つ。

  • Cognitive Search の Uri
  • インデックス名
  • API キー

Cognitive Search の Uri とインデックスの名前は、Azure Portal でCognitive Search のリソースを開き、メニューの Overview (概要) を開くと Uri を確認できます。また、下の方の Index のタブをクリックすればインデックス名が確認できますので、これから更新したいインデックス名をメモしておきます。

API キーは、ポータルで Keys のメニュー開くと確認できます。admin keyがインデックスを操作するときに必要なキーです。ちなみに下の方にある query key は検索に使えるキーですので、今回はこちらではありません。

SearchClient のインスタンス化

Index の更新には SearchClient のインスタンスが必要になります。生成は以下コードのように何パターンかありますが、用途に応じて使えばよいです。

インデックスの更新

公式ドキュメントだと以下のリンクに書かれています。翻訳されたタイトルがインデックスを読み込む ってなってるので混乱しますが、データを Cognitive Search の index に読み込ませるコードです。

ただ、このドキュメントに書かれてるやり方はいまいちめんどくさいので、SearchClient には更新するのには、UploadDocumentsAsync メソッドでよいかなって感じです。コレクションをメソッドの引数にいれるだけなので、上記リンクのドキュメントのサンプルよりシンプルに書けます。

// インデックス更新に必要なドキュメントのコレクションを取ってくる適当なメソッド `GetSampleItems();` があるとして
var items = GetSampleItems();
// これだけで更新完了
var response = await searchClient.UploadDocumentsAsync(items);

これは Upload しか使わない場合ですが、MergeOrUpload のメソッドもあるので汎用的に使えます。

JSON Serializer のカスタマイズ

冒頭で JSON の Serializer の扱いには注意とふれましたが、このブログで書いておきたかったのがカスタマイズ方法です。

前提として デフォルトの v11 の SDK は

  • System.Text.Json に依存しています。
  • C# のコードのプロパティのままシリアライズするため、(命名規約に沿ってコーディングしていれば) Pascal case でシリアライズされます。

Json 扱ってるなら camel case やろってのが一般的だと思ってますので、camel case にシリアライズするよう設定します。そしてSystem.Text.Json と Newtonsoft の 2つのパターンを書いていきます。

System.Text.Json の Serializer で camel case に設定する

なんらかの理由で Newtonsoft させたいことがない場合は、デフォルトの System.Text.Json で camel case を設定すればよいです。

実装!

コードはこんな感じ。SearchClient のインスタンスを生成する際、searchClientOptions を用意してあげるだけですね。

Newtonsoft の Serializer で camel case に設定する

NuGet パッケージの追加

追加する NuGet は以下です。

実装♪

コードはこんな感じ。SearchClient のインスタンスを生成する際、searchClientOptions を用意してあげるってのはお決まりのぱたーんだけど、中にセットしてる Serializer が異なる。

終わりに

雑な Factory 作るとしたらこんな感じですかね。そもそも同一プロジェクトで3つのパターンを利用する必要性はないと思いますがw

上のコードで使ってる options も雑にこんな感じで。ASP.NET Core とかだと DI で入れてあげればよいですね。

Swagger UI で Azure AD の認証をする (ASP.NET Core, Authorization Code Flow with PKCE)

前回は Open API の基本的な設定をしましたが、今回のゴールはこんな感じ。

  • Swagger UI の Authorize ボタンから Azure Active Directory (Azure AD) のサインイン画面にとんで、サインインできたらトークンを取得する
  • Swagger UI で認証が必要な API を Authorization ヘッダー付きでコールできるようにする
  • Web API は ASP.NET Core 5.0 (3.1 でも設定はほぼ一緒、一部の NuGet package のバージョンが異なるくらい)
  • IdP は Azure Active Directory で、認証フローは Authorization Code Flow with PKCE

セットアップの方法を書いていきますが、最終的なサンプルのコードはこちらにおきました。

https://github.com/beachside-project/aspnet-core-openapi-sandbox/tree/main/src/AspnetCore50WithAzureAdAuthorizationCode

Azure AD の設定

まずは Azure AD をセットアップします。Web API で Authorization Code Flow with PKCE で認証を通過させたいので、id token だけじゃなく access token で認証を通るようにしておきましょう。

Azure AD のテナントとユーザーの準備

テナントがなくて新たに作りたい方はこちらのドキュメントをご参考に作成してみるサクッと作れると思います。

また、のちほどログインして検証するのでユーザーを追加したり招待したりして用意しておきましょう。

ユーザーは追加するとアカウントが一つ増えてパスワードの管理もひとつふえるので、既になんらかのアカウントがある場合は招待(=新しいゲスト ユーザーの追加)をした方がよいです。

アプリ登録

Azure Portal で Azure AD のテナントを開いて アプリ登録新規登録 をクリックします。

f:id:beachside:20210122152900p:plain

以下を参考に値をセットして登録します。

  • 名前 : 適当にわかりやすいものを入れます。ここでは swagger-auth-sample といういまいちな名前にしました。
  • サポートされているアカウントの種類: シングルテナントにするかマルチテナントかにするかってやつです。今回はなんでもよいですが、シングルテナントの設定にしました。
  • リダイレクト URI: ここは間違えると死ぬ重要なポイントです。Swagger UI へのリダイレクトを設定します。プラットフォームは シングルページアプリケーション (SPA)を選びます。で URI はここでは https://localhost:5001/swagger/oauth2-redirect.html にしています。まずは Web API でデバッグ用をセットしてます。base url + swagger/oauth2-redirect.html の値です。私の環境だと ASP.NET Core でデバッグする際は https://localhost:5001/ で起動するのでこの値です。

f:id:beachside:20210122153043p:plain

登録すると、作成した app が開かれた画面になります。 認証 をクリックして設定内容を確認しておくと、プラットフォームが SPA で指定した Redirect URI が登録されていることと、Implicit flow が無効になっていることくらいでしょうか。

f:id:beachside:20210122153755p:plain

アプリ登録 > API の公開

access token で認証できるように API の公開をします。先ほど作成した app のメニューの API の公開 > Scope の追加 をクリックします。

初めて Scope を追加するときは アプリケーション ID の URI の登録画面がでます。値はデフォルトのままでよいです。保存してから続ける をクリックします。

f:id:beachside:20210122154150p:plain

スコープの追加の画面に遷移しますので、適当に値を入れて スコープの追加 をクリックして追加します。スコープ名はなんとなくで user_impersonation って名称にしました。サンプルの API を作るだけに深い意味はありません。

f:id:beachside:20210122154806p:plain

アプリ登録 > API のアクセス許可

今作成したスコープにアクセスできるようにします。API のアクセス許可 > アクセス許可の追加 をクリックします。

f:id:beachside:20210122154904p:plain

自分の API をクリックすると、自身が API を公開してる app の一覧が出てきます。今回いじってる swagger-auth-sample をクリックします。

f:id:beachside:20210122155039p:plain

アプリケーションに必要なアクセスの許可の種類は "委任されたアクセス許可" を選びます(もう一方は選択できませんがね)。
アクセス許可でさきほど作成した user_pmpersonation にチェックを入れて、アクセス許可の追加 をクリックして追加します。

f:id:beachside:20210122155255p:plain

アプリ登録 > マニフェスト

メニューの マニフェスト を開いてaccessTokenAcceptedVersion2 にセットします。保存 ボタンは忘れずにクリックします。

f:id:beachside:20210122164058p:plain

この設定自体は必須ではないんですが、accessTokenAcceptedVersion が null の場合( 1 として扱われる)と 2 の場合で Audience の設定方法が異なります。今回は今後を見据えて v2 になってる前提で設定方法を書いていくため、ここで設定をしておきました。

参考までにドキュメントはここら辺に書かれています。注意書きを読むと状況のカオス具合と 2 にしておこってお気持ちになります。

Azure AD の情報をメモ

以上で Azure AD の設定は完了です。あとは、ASP.NET Core の Web API に必要な情報をメモっておきましょう(Azure AD の画面を開いておけばよいだけですが共有だけしておきます)。

まずさきほど作成した app の概要を開きます。

f:id:beachside:20210122161821p:plain

Web API を構築する上で必要なのは ClientId と MetadataAddress の2つ。

  • ClientId: 概要ページの アプリケーション(クライアント) ID の値

次は、エンドポイント をクリックします。 f:id:beachside:20210122162244p:plain

  • MetadataAddress: OpenID Connect メタデータ ドキュメント の値

Swagger UI を構築する上で必要なのは "Authorization endpoint" 、"Token endpoint" 、"Scope の full name"3つ。

  • AuthorizationUrl: "OAuth 2.0 承認エンドポイント (v2)"の値
  • TokenUrl: "OAuth 2.0 トークン エンドポイント (v2)" の値

  • Scope の full name: メニューの API のアクセス許可 をクリックして先ほど登録した user_impersonation をクリックすると表示される "api://” から始まる値です。

f:id:beachside:20210122162716p:plain

ASP.NET Core で Web API を作成

ここからは、ASP.NET Core 5.0 の Web API を作成していきます。

ちなみに Swashbuckle.AspNetCore のバージョンは現時点で最新の v5.6.3 を使っています。

Web API プロジェクトの作成

Visual Studio 2019 で新しいプロジェクトを作成します。プロジェクトテンプレートは、C# の ASP.NET Core Web アプリケーション を選んで次に進みます。

f:id:beachside:20210122165243p:plain

プロジェクト名とかは適当に入れて 作成 をクリックします。

f:id:beachside:20210122165428p:plain

後は下図のように選択します。

  • ASP.NET Core 5.0 が選択できない場合は VS のバージョンが古いので更新しましょう。
  • 5.0 だと Enable OpenAPI support のチェックを入れることで Swashbuckle の設定がされた状態になります。
  • ASP.NET Core 3.1 で作成したい場合は、Swashbuckle の設定を自分でやる必要があります。前回のブログを参考に の "1. Swagger の導入設定(Swashbuckle)" と "2. デバッグ開始時に Swagger UI を起動する" に設定方法が書いてます。

f:id:beachside:20210113163827p:plain

appsettings.json の編集

ソリューションエクスプローラーで appsettings.json を開きます。

f:id:beachside:20210122170140p:plain

デフォルトで LoggingAllowedHosts がセットされています。今回は AzureAd のオブジェクト定義してその中に Azure AD の先ほどめもった情報をセットします。"apiScope" には前述の "Scope の full name" を入れておきます。スコープについてはガチだと複数になる場合もあるので配列にした方がよいとかありますが今回はシンプルに文字列で定義してます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AzureAd": {
    "clientId": "",
    "metadataAddress": "",
    "AuthorizationUrl": "",
    "tokenUrl": "",
    "apiScope": ""
  },
  "AllowedHosts": "*"
}

Startup.cs で認証・認可を設定

Startup.cs を開いて ConfigureServices メソッドで認証のコードを追加します。

ASP.NET Core 3.1 の場合、NuGet の Microsoft.AspNetCore.Authentication.JwtBearer をインストールします。5.0 だとデフォルトでインストールされています。

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
        {
            options.MetadataAddress = Configuration.GetValue<string>("AzureAd:MetadataAddress");
            options.Audience = Configuration.GetValue<string>("AzureAd:clientId");
        });
//以下省略

Web API の認証の設定は最低限だとこれだけで実現できます。ケース次第で Token の validation のカスタマイズとか、Issuer の validate とか設定する必要もあるかとは思いますが、今回はシンプルに最低限の実装。

次に Configure メソッドの app.UseAuthorization(); の上に app.UseAuthentication(); を追加します。

// 略
  app.UseAuthentication();  // これを追加
  app.UseAuthorization();
// 略

Controller のカスタマイズ

雑に認証が必要な API を追加しておきましょう。デフォルトで作成される WeatherForecastController.cs を開き、以下のメソッドを足しておきましょう (using ステートメントはよしなに足してください)。

[Authorize]
[HttpGet]
[Route("get2")]
[Produces("application/json")]
public IEnumerable<WeatherForecast> Get2()
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
        .ToArray();
}

デバッグ実行

認証で保護されてアクセスできないことを確認するために、デバッグ実行してみましょう。デバッグを開始すると Swagger の画面が立ち上がります。

デバッグ実行で base url が https://localhost:5001/ ではない場合、前述で設定した Azure AD の app の認証でリダイレクト URI を正しい値に変えるか、Visual Studio でデバッグの URL (ポートだけだと思いますが) を変えて、この二つの値が同一になるように合わせる必要があります。

Try id out をクリックします。

f:id:beachside:20210122172421p:plain

Execute をクリックすると実行した Curl のコマンドや Response が表示されます。Curl のコマンドでは Authorization ヘッダーがついてないことが確認できます。また、レスポンスは 401 が返ってきてるので、正常に動作していることが確認できます。

認証関連の動作確認は、まず認証できないことの確認が重要ですね。

f:id:beachside:20210122173255p:plain

Swagger で認証の設定

ここから Swagger UI の Authorize ボタンをクリックすることで認証してトークンを取得できるように、2か所の設定をします。

Startup.cs を開きます。

まず1つめは ConfigureServices にある services.AddSwaggerGen(c => の部分を以下のようにします。

もうただの設定なのでこうするだけやって感じではあるんですが、すべての API でこの認証を使うように c.AddSecurityRequirement の部分を書いています。

あとは、Scopes には必要なスコープを入れましょうってくらいでしょうか。"openid" はこのブログの文面だけだと別に使いませんが、予期せぬトラブルシュートとかの時に id token も検証には役立つこともあるので、個人的にはいつも入れてます。

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "AspnetCore50WithAzureAdAuthorizationCode", Version = "v1" });

    c.AddSecurityDefinition("Azure AD - Authorization Code Flow", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri(Configuration.GetValue<string>("AzureAd:AuthorizationUrl")),
                TokenUrl = new Uri(Configuration.GetValue<string>("AzureAd:tokenUrl")),
                Scopes = new Dictionary<string, string>
                {
                    ["openid"] = "Sign In Permissions",
                    [Configuration.GetValue<string>("AzureAd:apiScope")] = "API permission"
                },
            }
        },
        Description = "Azure AD Authorization Code Flow authorization",
        In = ParameterLocation.Header,
        Name = "Authorization",
    });

    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id ="Azure AD - Authorization Code Flow"},
            },
            // Scope は必要に応じて入力する
            new string [] {}
        },
    });
});

2つめは、Configure メソッドの中の app.UseSwaggerUI(c => の部分を以下のように変えます。

ここもポイントは特にないんですが、このメソッド自体が、if (env.IsDevelopment()) の中にあるので、別の環境でも Swagger を表示させたい場合は if の条件をカスタマイズする必要があるくらいでしょうか。

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerSandboxAspnetCore31WithAzureAd v1");
    c.OAuthClientId(Configuration.GetValue<string>("AzureAd:clientId"));
    c.OAuthUsePkce();
});

動作確認

デバッグ実行してみましょう。ブラウザーが起動すると思いますが、認証が絡む場合の動作確認はシークレットウインドウ的なのを起動して swagger UI を表示した方がよいです。理由はあるあるな話で cookie や session storage とかの情報を共有されると認証の確認がしにくくなる場合があるからですね。

私の場合、下図は律儀(?)に Microsoft Edge の InPrivate ウインドウでやってます (たまたまこれやってる時にクライアント側の検証も合わせてやっててそっちに Chrome のシークレットウインドウを使ってるだけです)

Authorize のボタンができてます。また 各 API に南京錠のロックが空いたアイコンがついてます。c.AddSecurityRequirement でこれらどれをクリックしても同じ認証を使うように設定してますので、とりあえず Authorize ボタンをクリックしてみましょう。

f:id:beachside:20210122175503p:plain

軽く説明を加えるとしたら、`client_id はデフォルトで設定してますのでいじらなくてよいです。client_secret は空っぽのままで大丈夫です。
今回 AAD 側の platoform を SPA で設定してるので入力するとエラーになります。ちなみに SPA で設定しないと、token request 時に CORS のエラーで怒られます。
まぁ Authorization Code Flow での設定ミスあるあるなやつですね。

Scopes は両方にチェックをいれて、Authorize をクリックすると、認証処理が開始します。

f:id:beachside:20210122180532p:plain

リダイレクトされて Azure AD のサインイン画面が表示されます。サインインしましょう。

f:id:beachside:20210122181537p:plain

無事にサインインできるとこんな画面になります。

f:id:beachside:20210122181441p:plain

各 API の南京錠もロックされたアイコンに変わっています。get2 を実行してみましょう。

Curl のコマンドで Authorization ヘッダーにトークンを付与して API をコールしてることがわかります。また、Response の status code が 200 で値が取得できていることがわかります。

f:id:beachside:20210122181737p:plain

こんな感じで認証付きの API も楽に Swagger UI から検証できるようになります。

エラーでうまくいかないときは...

うまくいかないときはなんらかの設定が間違ってる可能性が高いのですがその時に私がやりそうな方法を紹介しておきます。

ブラウザの DevTools で確認

とりあえずさっと見れるのは、Chrome や Microsoft Edge だと F12 キーで起動する DevTools の Console でエラーを確認することです。下図だとエラーでてないですが(汗)。ここで CORS のエラー出てたら Azure AD の platoform が SPA 以外に設定してまってるやんとか切り分けることができます。何のエラーだとなにだってのを論理的にわかるようになるには多少の知識は必要ですが、とりあえず原因かもしれない可能性を見ることでできるかもしれない部分です。

f:id:beachside:20210123024240p:plain

OnAuthenticationFailed event をキャプチャする

OnAuthenticationFailed をキャプチャすることでエラーが見やすくなることがあります。初歩的なミスの場合はここでわかるかなーってのが私の個人的な印象。

まず、Startup.cs を開きましょう。

このメソッドを追加します。このコードのエラーメッセージの表示だけでも十分ですし、ブレークポイントも貼っておいて AuthenticationFailedContext の中身を見るってのがわかりやすいです。

private static async Task AuthenticationFailed(AuthenticationFailedContext arg)
{
    var message = $"AuthenticationFailed: {arg.Exception.Message}";
    arg.Response.ContentLength = message.Length;
    arg.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
    await arg.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(message), 0, message.Length);
}

あとは、.AddJwtBearer 拡張メソッドでこんな感じにセットします。

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.MetadataAddress = Configuration.GetValue<string>("AzureAd:MetadataAddress");
        options.Audience = Configuration.GetValue<string>("AzureAd:clientId");
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = AuthenticationFailed
        };
    });

ある程度こなれてくるとここではエラーがヒットしなくなります。

ネットワークをキャプチャして認証のリクエストを確認

Authorization Code Flow など認証は画面では見えないところで request を送信してます。フリーのツール(Fiddler 4 とか)を使ってネットワークをキャプチャして、リクエストとそのレスポンスやエラーをみることで原因を突き止めやすくなる場合もあります。

Fiddler 4 だとダウンロードリンクはここでいいのかな (最近新たに DL してないからわからので自己責任でお願いします) Download Fiddler Classic

よくみるのは、authorization request の url やクエリパラメーターが正しいかとか...

f:id:beachside:20210122182850p:plain

token request のresponse body にエラーがはかれてることもあります(下図は正常な response bodyですが)。

f:id:beachside:20210122183000p:plain

ここまで client id とかにフィルターをかけてきたけど、最後に別にいいやってお気持ちになった

完成版のサンプルコード

ということで今回の完成版のコードは GitHub のこちらにおいてあります。

https://github.com/beachside-project/aspnet-core-openapi-sandbox/tree/main/src/AspnetCore50WithAzureAdAuthorizationCode

おわりに

個人的な経験から感じるのは、エラーを愚直に見るとエラー自体が見当違いのエラーのためハマることも多い認証系の開発ですが、認証フローをある程度理解することでエラーからどの問題やろかってのが理解できるようになるかなぁと感じています。

結局参考にしたのがここだけでしたのでちょっとハマりました。

GitHub - domaindrivendev/Swashbuckle.AspNetCore: Swagger tools for documenting API's built on ASP.NET Core

余談: Azure AD 開発ウェビナーあるってよ♪

Azure AD の開発にこんなウェビナーがあります。Azure AD とは的なとこからなので5時間くらい?のがっつりしたのですが...まぁ全部見なくても、開発のところだけ見ていけば 1 ~ 2hとかな気がしますので、これから Azure AD 開発に入門したい方は観てみるとちょっとは参考になるかもしれません♪

Cosmos DB の 整合性レベル ( Consistency levels ) を改めて整理してみた

Cosmos DB の整合性レベルは、個人的にはいつもはあまり意識せずに Session を使ってます (意識する必要のない使い方や設計をしているって言った方が妥当か..)。

ただ、Document DB が出た時からもう4-5年使ってるのに、整合性レベルの日本語表記の英語の表記は頭のなかで一ミリのピンとこない (むしろ日本語表記の名称知らん) ので、それを整理したくてメモしてみました。

英語と日本語の表記

英語と日本語はドキュメントと Azure ポータルの表記を書いてみました。

英語 日本語 (ドキュメント) 日本語 (Azure ポータル)
Strong 厳密 強固
Bounded staleness 有界整合性制約 (←同じ)
Session セッション (←同じ)
Consistent prefix 整合性のあるプレフィックス 一貫性のあるプレフィックス
Eventual 最終的 (←同じ)

なんとゆーことでしょう...本日時点では表記違うし (私にとっては) その日本語の固有名詞では意味がピンとこないです。

(ポータルは基本的に英語にしてるので、ドキュメントとの表記の揺れがあるのに今気づいた...)

料金・コストの違い

直接的な価格体系に違いはないです。ただし、Strong および Bounded Staleness の Read の RU が 2倍になると、以下のドキュメントに書かれているので見ておきましょう。

ローカル マイノリティの読み取りに対する RU/秒の読み取りコストは、強度の低い一貫性レベルの場合の 2 倍です。これは、2 つのレプリカから読み取りが行われ、強固および有界整合性制約の整合性が保証されるためです。

整合性のセマンティクス

細かい仕様は公式のドキュメントを読んだ方が2億%よいので、ここではさらーっとざっくりとひとことで説明できる程度に整理しておきます。

Strong (厳密 / 強固)

全てのレプリカで最終的なコミットされたデータのみが Read できる。その分処理も遅くなるのは仕方ない。

個人的には、これを求めた時点で No SQL の DB 使わなくてよくね?とか厳密なトランザクションを必要とするならそれに強い DB 使うべきじゃね?とかアーキテクチャーに疑問を感じそう。(もし理由を聞いたら聞いてみたら妥当な場合もあるとは思いますがね)

Bounded staleness (有界整合性制約)

"K" 回の Write、またはレイテンシーの時間 "T" だけ Read が遅延する可能性があります。で、 "K" と "T" を設定可能です。レプリカの有無で設定できる値が変わります。Write できるリージョン数が単一だと同じリージョンのクライアントの整合性は Strong になるってのがある。

Write できるリージョン数と、レプリカが同一リージョンか別リージョンかによってちょっと制約はあるので詳しくは公式のドキュメントをチェック。

Session (セッション)

Cosmos DB 作成時のデフォルトの設定値。

Monotonic Read (同一のセッションでは Read においてデータの先祖がえりはしない), Monotonic Write (同一のセッションではデータの Write 順序は保証される) , Read Your own Write(RYW: 同一セッションでは Write したデータは即データが Read できる)を保証するってものです。

Write できるリージョン数と、レプリカが同一リージョンか別リージョンかによってちょっと制約はあるので詳しくは公式のドキュメントをチェック。

Consistent prefix (整合性のあるプレフィックス / 一貫性のあるプレフィックス)

Write が複数発生した場合の順序保証はあるけど、更新された最新のデータが即時 Read できる保証はない (もちろん最終的には Read できる)ってものです。

(毎回のように書いてるけど) Write できるリージョン数と、レプリカが同一リージョンか別リージョンかによってちょっと制約はあるので詳しくは公式のドキュメントをチェック。

Eventual (最終的)

Write が複数発生した場合の順序保証なし、レプリカはそのうち最終的に同期さます。ゆーてもよっぽど激しい Write じゃない限り数秒で収束すると思いますが。

参考

さっとひとことで説明できる程度にまとめただけ (全然ひとことになってないけど...)なので、詳細は公式ドキュメントで理解しましょう。

docs.microsoft.com

Cognitive Services: Read API 3.x - OCR の API を試す (2020/10月)

2020 年は1月から9月の間で Cognitive Services の Vision カテゴリーの中の OCR の機能がちょろちょろとアップデートしてました。
OCR の今までのアップデートを振り返りつつ、最新の Read API v3.1 Preview2 を試してみます。

Cogbot #29でもお話しした内容ですが、資料らしい資料を作ってなかったのでブログでサンプルコードなどをフォローしてみます。

🚀 Cognitive Services の OCR の歴史

OCR の機能は Cognitive Services の初期から存在

  • 日本語も対応してた
  • Computer Vision API v2.1 までは 3つの API があった: "OCR"、"Recognize Text"、"Batch Read File“

Computer Vision API v3.0 時代の到来

  • 2020年1月に Public preview で Read API が公開。この API が以前の "OCR" の API より OCR の精度はめっちゃよくなったが、対応言語が英語とスペイン語のみ (´-ω-`)...
  • v2.x であった “Recognize Text”、“Batch Read File” は v3 で亡くなった。以前からある “OCR” は残ってるけど非推奨扱い。
  • 旧 API を利用している方向けのアップグレードのフローのドキュメントはある。
  • 2020年5月にユーロ圏の言語追加と精度向上。
  • 2020年7月にv3.1 で中国語(簡体)、2020年9月に日本語が追加

余談

今回のブログではふれませんが、Cognitive Services の OCR で帳票特化の Form Recognizer があります。2020年9月の Ignite で GA しました。
日本語はまだ未対応ですが今回 Read API で日本語の対応が入ったので、Form Recognizer も対応する時期は近いかもしれませんね...と期待してます。

🚀 Read API を試す

サブスクリプションキーとエンドポイントの取得

Azure ポータルからサクッとリソースを作ります。以下のドキュメントのまんまやればいい感じなので説明は省略します。補足があるとすれば、キーには2パターンあります。

  • Multi-service resource: ドキュメントに記載がありますが複数のサービスをいっぺんに利用できます。課金とかもまとまるので楽です。
  • Single-service resource: Multi-service で利用できるサービスが個別でしか利用できません。

どちらがよいとかないので、用途に応じて使えばよいです。ものによってはエンドポイントや送信に必要な情報が微妙に変わったりするので、使うサービスに応じてエンドポイントや設定する内容をドキュメントで確認する点だけは気を付けましょう。

docs.microsoft.com

キーのリソースを作成したら開きましょう。下図は Multi-service resource のキーを作成したときの画面ですが、Keys and Endpoint を見るとサブスクリプションキーとエンドポイントが見えます。これを後述の実装編で使います。

f:id:beachside:20201009192050p:plain

HttpClient を使った実装編

SDK を利用してサクッとアクセスしたいところでしたが日本語対応の v3.1 preview2 の API に投げてくれないので、2020年10月時点では HttpClient を使って API にアクセスします。
雑にサンプルコードを書いてみました。
C#、.NET Core 3.1 でコンソールアプリで作っています。 TODO が3つあるので、必要な情報を埋め込んで実行すれば動きます。

普段仕事で100%書かないコメントをコードに書いてみました。 (状況や理由みたいなコメントは書きますが、メソッドの英語を翻訳しただけやんってのを日本語でコメント書かないって意味です)
ブログとかだとコードにコメント書いて説明した方が簡単だなって今更気づきました。

ちょっとだけ補足すると...

  • 48行目で Read API をコールして正常の場合、HttpStatusCode は 202 (Accepted) が返ってきます。渡した URL が Read API から読み込めなかったり画像を認識できなかったりすると BadRequest が返ってきます。
  • 82行目のステータスの確認は、最近仕様が変わりました。SDK に Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models.OperationStatusCodes ってあるんですが、API の仕様が変わってステータスの文字列が変わった(以前はパスカルケースだったのがキャメルケースになった)のでそこも今はとりあえず使わず API の Reference) みて正しい評価をしてあげるとよいです。
  • 今回は画像の URL を指定しましたが、ローカルの画像を送信したい場合は普通にヘッダーのMIMEタイプ を application/octet-stream にして body に binary をつけて送信すればよいです。

SDK を使った実装編

SDK は、NuGet package の *Microsoft.Azure.CognitiveServices.Vision.ComputerVision** です。
2020年10月時点で 最新の v6.0.0-preview.1 をインストールします。
前述でちょっとふれましたがこの SDK は Read API の v3.0 のエンドポイントへアクセスしています。そのため日本語の画像を送っても文字を認識しません。動作確認をするには英語とか対応してる言語の画像を投げましょう。

つまり日本語の対応版の v3.1 preview2 を動かすことでできません。まぁそのうち対応すると思うのでこっちも試して雰囲気を見てみましょう。
HttpClient でやってくれてたやつをなんか型もってやってくれてるんだなーって雰囲気はつかめると思います。

参考

Python + Azure Functions 入門: タイマートリガー 編

前回は Python で Azure Functions をローカルで開発するときの環境セットアップと、HTTP トリガーの Function App を作りました。

blog.beachside.dev

今回はタイマートリガー、つまり一定の時間間隔で起動するような Function App を作ってみます。

時間指定とか特定の間隔で動くバッチ的なものは、クラウドを使う中ではレガシーなアーキテクチャーを使っている際のイケてないパターンなことが多いと個人的に思ってます。

アーキテクチャーの議論は本題ではないので書く気はないですが、イベントドリブンでガンガン動かしてしていく方がいろんな意味でよいということだけは雑に書いておきます。バッチ処理という概念を持ってる時点でレガシー、そして多くの場合何も考えてない。もちろん必要なケースもあるんでしょうけど(知らんけど。

とさんざんディスってる空気がある中、必要に駆られることもありそうだなーと思い続けていきます。

Azure ポータルから作成を...

Azure ポータルから作成もできますが、ここではしません。ローカルデバッグしたいし。

VS Code から Function App を作成

準備

前提として、前回のブログ でやったような開発環境の準備は終わっていることが前提です。

プロジェクトの作成

VS Code のターミナルで適当なディレクトリを開き以下のコマンドで Function App のプロジェクトを作ります。

func init HelloTimerProject --python

f:id:beachside:20200908120021p:plain

作成されたプロジェクトのフォルダ HelloTimerProject をVS Code のルートとして開きなおします。

code HelloTimerProject -r

仮想環境のアクティベート

必要に応じてやっておきます。私の今の環境だと python を動かすコマンドが python なので

python -m venv .venv

からの

.venv\scripts\activate

でアクティベートされていることを確認します。

f:id:beachside:20200908130129p:plain

タイマートリガーの Function を追加

このプロジェクトにタイマートリガーの function を追加します。追加するコマンドは、テンプレートを指定せずに function の名前だけ指定してみましょう。以下図のようにテンプレートを選択できます。ほかのトリガーの function を作りたいならここから選ぶだけです。今回は Timer trigger を選び、Enter キーを押します。

f:id:beachside:20200908123332p:plain

スケジュールの設定

作成されたフォルダの中を開き、トリガーの定義が書かれている functions.json を見てみます。

f:id:beachside:20200908130504p:plain

9行目がスケジュールを定義している部分です。NCRONTAB 式で表現します。ほかの形式でも書けますが制約があるので基本的には NCRONTAB でよいかなと思ってます。

NCRONTAB 式は、迷ったら以下のドキュメントを参考に書けばばっちりです。

function.json の構成

function.json で設定できるプロパティは以下のドキュメントに記載があります。

よく使いそうなのは runOnStartup でしょうか。これを true に設定すると、function のホスト起動時にもエントリーポイントが実行されます。ローカルでデバッグする際、起動後一定時間待つのはつらいので true にセットすることで便利に扱えます。今回は上図の9行目で設定しています。

エントリーポイントである init.py を開くと、ログをはいてるだけのプログラムとなります。これをデバッグで実行して runOnStartup の設定により起動時にも main メソッドが起動してログがはかれるか確認してみます。

デバッグ実行

以下のコマンドでデバッグ実行してみましょう。

func start

ログをみてわかる通り、Function ホストの起動時に main メソッドが起動してログが出ています。その後は 1分おきに main メソッドが動いてログが表示されています。

f:id:beachside:20200908132325p:plain

開発における補足

Function App をタイマートリガーでバッチ起動したところで、従量課金プランだとデフォルト5分、最大10分でタイムアウトしま。タイムアウトは host.json で設定しできます。

10分あれば十分だと思いますが、実行時間の長いバッチなんだよーと悩む方は、function をわけて Queue などを使ってチェーンさせましょう。ワークフローの管理が大変になりそうだったら Durable Functionsという選択肢もあります。

デプロイ

これは前回のブログの後半VS Code からデプロイとさほどかわらないのでここでは触れません。

まとめ

以下2つのドキュメントを参考に開発すればばっちりです。

docs.microsoft.com

docs.microsoft.com

Azure AD で ゲストユーザー が所属するグループのメンバー一覧が見えてしまうのを制限する

Azure Active Directory (Azure AD) に外部のゲストユーザーを招待すると、招待されたユーザーは アクセスパネル からグループに属しているユーザー全員の ID (メールアドレス) を見ることができました。以下図のようにがっつりみえます(ぼかして見えてねーわってツッコみたくなる気持ちはグっと抑えてください)。

f:id:beachside:20200908010732p:plain

外部ユーザーゆーても同じ組織に属してるんだからまぁまぁ...といいたいところですが、気にする人いるわけで微妙だと感じてましたが、これを見えなくなる機能ができたようなので試します。

補足として、このグループとは Azure AD の Group です。Group についてはこちらのドキュメントにありますが、まぁ想像できそうな感じの機能です。

Azure Portal から設定する

Azure Portal で Azure AD を開き、ユーザー設定 > 外部コラボレーションの管理をします をクリックします。

f:id:beachside:20200908003602p:plain:w600

ゲストユーザーのアクセス制限 は、デフォルトでは真ん中が選択されていますが、一番下を選択します(①)。あとは保存をクリックします(②)。

f:id:beachside:20200908011544p:plain:w600

設定の反映まで15分くらいかかることがあるようなので気長に待ちます。

アクセスパネルを確認してみる

なんということでしょう。さきほどががっつり見えていたユーザーたちは、グループの一覧を開いた時点で見えなくなりました( ・ㅂ・)و (劇的ビフォーアフター!

f:id:beachside:20200908005348p:plain

Azure Portal からももちろん見えません ( ・ㅂ・)و

f:id:beachside:20200908010305p:plain

まとめ

最近 Azure AD B2C のコンシューマー向け機能を取り込みがちな前のめりな空気を感じる Azure AD ですが、外部のユーザーを招待する場合はケースバイケースでこの機能は使うと多少幸せになりそうです♪

参考

docs.microsoft.com

Python + Azure Functions 入門: Http トリガー 編 ( Windows10 )

普段から C# で Functions をがっつり使ってる私ですが、たまには Python での入門ネタでも整理しようかと思った今日この頃です。

今回は、Python で HTTP リクエストを受けてレスポンスを返す Azure Functions を作ります。簡易な Web API ですね。デバッグ実行からデプロイして動作確認するところをやっていきます。

環境は Windows 10 と VS Code を使っていきます。WSL2 の Ubuntu 上での実行ではなく、素の Windows 上で python を動かす環境という点だけ注意してください。素の Windows 環境と WSL2 / Ubuntu で動作させるときとの違いはツールのインストール方法が異なるくらいだとは思いますが....どうでしょうね。。。

Azure Functions とは

Azure Functions または Function App と言い表していることが多いです。
(日本語に翻訳されて関数アプリとか関数って表現は微妙やなと思っています。)

Azure の Serverless なサービスで、HTTP リクエストをトリガーにしたり Azure の他サービスのイベントをトリガーに動かすことができるバックエンドのサービスです。

似たようなサービスは、AWS の Lambda や GCP のCloud Functions です。

サーバーレスなので、従量課金で使えます。めちゃ安い。検証程度だとほぼ無料で使える従量課金だと、どのサービスでもそうですが当たり前にコールドスタートします。もちろんコールドスタートをしないようにプランを上げるとか工夫するとかの手段はあります。

このブログ書いてる時点では C#、Java、JavaScript、Python、PowerShell での実装ができます。C# が SDK が充実していて使いやすいってのはありますが、JS や Python の対応の勢いを感じる昨今です。

公式ドキュメントの概要はこちら。

Azure Functions の概要 | Microsoft Docs

細かいことはさておきまずは動かしたいので、今回は HTTP Trigger での Function App を作ってみましょう。

開発環境の準備

Node.js

npm のパッケージを使うので必要です。Node の環境なんてないって方向けに、先日私が node の環境を構築したメモが偶然にもあります。

blog.beachside.dev

Azure Functions Core Tools

Function App を作ったりデバッグしたりするのに使う開発の中心となるツールです。npm からインストールしましょう。現時点では v3 がデフォルトです。コマンドプロンプトかなにかで以下のコマンドを実行してインストールします。

npm install -g azure-functions-core-tools@3

f:id:beachside:20200812210436p:plain

Azure Functions の VS Code Extension

Azure Functions をデプロイをするのに使います。VS Code の左側のメニュー: Extensions > 「azure functions」と入力すると Microsoft が提供している Azure Functions が出てきます。インストールしましょう。

f:id:beachside:20200813184734p:plain

Azure Storage Emulator

Azure Functions の起動には Azure Storage が必要です。ローカルでデバッグする際は Azure Storage のエミュレーターを使います。
Azure を使った開発をしている人はだいたい入ってるはずですがインストール済みか不明って方は Windows のメニューで「Azure Storage Emulator」と入力してインストールされているか見てみましょう。なければ以下のドキュメントを参考にインストールします。さらに起動についてもドキュメントに書いてあるので忘れず起動しましょう

開発とテストに Azure Storage Emulator を使用する | Microsoft Docs

Python 環境

Azure Functions Core Tools では、Python の versionは 3.8 を用意しておくと問題ないです。本題じゃないのでここでは書きませんが VS Code で Python が動かすための設定をしてない場合はしておきましょう。

code.visualstudio.com

この後からがようやく本題です。

HttpTrigger の Function App をつくる

VS Code の GUI で操作して作る方法と CLI で作る方法があります。GUI がめちゃいいってわけでもないので CLIでやります。

コマンドプロンプトや PowerShell を使っても問題ないですが、せっかくなので VS Code を立ち上げて Ctrl + Shift + @ キーを押すとターミナルが起動しますので、そこでやりましょうか。ターミナルを立ち上げたら自分の作業用のフォルダに移動しておきましょう。

プロジェクトの Initialize

以下のコマンドを打って HelloFunctionAppProj という名前のプロジェクトを作ってみます。これで Function App のベース部分だけできます。

func init HelloFunctionAppProj --python

f:id:beachside:20200813133531p:plain

VS Code の Explorer で、今作成した HelloFunctionAppProj フォルダをルートとして開き直したほうが後々面倒がないので、以下のコマンドを打ちます。

code HelloFunctionAppProj -r

仮想環境の Activate

今開いているはずの HelloFunctionAppProj のフォルダに対して venv を activate します。
私の今の環境だと以下のように python ってコマンドでやりますが、環境次第では py だったりすると思います。ご自身の環境のコマンドで activate すれば大丈夫です。

python -m venv .venv

そして、

.venv\scripts\activate

これで activate できます。

f:id:beachside:20200813201027p:plain

HTTP trigger の function 作成

プロジェクトに HelloHttp って名前の HTTP trigger の function を追加します。以下のコマンドうちます。

func new --name HelloHttp --template "HTTP trigger"

f:id:beachside:20200813133843p:plain

これで以下のフォルダ構成になります。プロジェクトフォルダの直下は functions を構成するファイルがあり、HelloHttp ってフォルダの下に function があります。

f:id:beachside:20200813211654p:plain:w300

プロジェクトの構成

Function App を作るうえで最初に知っておきたいことを2つ紹介します。

functions.json

Function App はデフォルトで __int__.pymain メソッドが呼ばれる構成になっています。こーゆー Function の構成は function.json で定義されています。以下のリンクは function.json のエントリーポイントの変更方法のリンクですが、このリンクのドキュメント全体を見ることで基本的な開発方法の情報を得ることができます。

Python 開発者向けガイド - 代替エントリーポイント

host.json

host.json は Azure のホスト側の設定を構成するためのものです。ログレベルもここで変更できます。詳細はこちらのドキュメントに記載があります。あとで Azure 上でログを見るのに logLevel を Information 以上で出力するように変えておきます。
(デフォルトで生成されたやつに logLevel の部分を追加しただけです)

{
  "version": "2.0",
  "logging": {
    "logLevel": {
      "default": "Information"
    },
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
}

デバッグ実行

__int__.py を開くと、どんなコードがテンプレートとして用意されているかわかります。HTTP リクエスト受け取ったら適当にレスポンス返してますね。ではデバッグしてこのコードを動かします。このコマンドを実行しましょう。

func start

f:id:beachside:20200813150755p:plain

エラーで動かないときは対処方法を後述してます。

これで、Terminal のメッセージの最後にローカルデバッグ中の Function への URL が表示されます。

f:id:beachside:20200813150928p:plain

__int__.py を見ると GET で name を受け取ったら読んでくれるようなので、上記の URL にクエリパラメーターを付けて、POSTMAN のような REST の Client かブラウザーで URL をたたくと無事に動いていることがわかります。

http://localhost:7071/api/HelloHttp?name=beachside

[f:id:beachside:20200813151117p:plain:400]

エラーで動かない? Could not find a Python version と表示されたときは...

デバッグ実行して「Could not find a Python version」というエラーが表示されることあるかもしれません。

f:id:beachside:20200813143318p:plain

原因として考えられる一つは Python 自体が認識されていない場合。python --version とコマンドを打って Python が認識されていることを確認します。

もう一つは .vnev を構成できてないとき。前述したとおりに .venv を構成しましょう。

Azure へのデプロイ

無事にデバッグで動作を確認出来たら Azure にデプロイしてみます。

Azure 上に Function App のインスタンスを作る

Function App のインスタンスの作成は、CLI でもできます (つまり自動化もできる) が初めての方だとわかりにくいと思い、一番わかりやすいと個人的に感じている Azure ポータルからやります。

Azure ポータルから作成する手順は以下のドキュメントの通りにやれば問題ないのでここでは書きません。

docs.microsoft.com

初めての方向けの情報として補足しておきたい点は、

  • リソースグループ: 超雑に説明するとプロジェクトの管理単位でフォルダみたいなものです。リソースグループを削除することでその中も一括で削除できたりしますので、とりあえずの検証で使うようなときは後で一括で削除する際の単位だと思ってもよいです。
  • リージョン: インスタンスを作るデータセンターの選択です。検証するだけなら「日本にいるから日本に」とか気にする必要ありません。私はほぼ West US 2 に作ります。同じサービスでもリージョンで料金が安かったりしますし(料金はあまり気にしないですが)、Preview 中のサービスとか日本の東西リージョンはリリースされないものもあるので。場合によりますがレイテンシーも気にするほどのものはありません。

他のポイントとして今回は Python の3.8 の Function App を作りたいので以下を選択します。

  • 発行: コード
  • ランタイムスタック: Python
  • バージョン: 3.8

f:id:beachside:20200813174730p:plain

余談ですが、Application Insights の設定も途中で出てきます。ここではさほど気にしなくてよいですが、Azure Monitor というサービス群の中の1サービスです。この Azure Monitor がログをリアルタイムに見たりアプリだけでなく Azure のインフラ側のログまでいい感じに収集したり、検索できたり異常発生時に通知を容易にしてくれる機能です。超便利で有用なので「そんな便利機能がある」程度に覚えておきましょう。

作成したらデプロイのプロセスが表示されますので、おわるまでしばし待ちます(1-2分くらいかなと)。

f:id:beachside:20200813180136p:plain

リソースグループを確認

作成が終わったら、リソースを見てみましょう。

f:id:beachside:20200813180219p:plain:w400

Function App のリソースが開きます。まだデプロイしてないので何もないですがとりあえずブラウザはこのままにして VS Code に戻ります。

デプロイと動作確認

VS Code からデプロイ

プロダクションレベルのコードは、GitHub Action や Azure DevOps の CI/CD ツールを使ってデプロイを自動化するのが常識ではありますが、お試しレベルであればサクッと VS Code からデプロイしましょう。

VS Code の左側のメニューで Azure tools のアイコンをクリックします。

サインインしてないとこんな画面がでるので、サインインします。 f:id:beachside:20200813191018p:plain:w280

FUNCTIONS を展開すると、先ほど作成した Azure の サブスクリプションの配下に Functions のリソースの名前が出てきます。不明の場合は前述で開いてた Azure ポータルの Function App のリソースを見ると、サブスクリプションや function App の名前が確認できます。対象の Function App を右クリックして Deploy to Function App... をクリックします。これだけでデプロイできます。

f:id:beachside:20200813185716p:plain:w600

ビルド・デプロイを考慮した開発とその先の検討

ここでは軽いコードのビルド・デプロイなので詳しく触れてませんでしたが、ガチな運用を考えると検討すべき点があります。

ビルドをリモート(つまりFunction App 上)で行う方法とローカルでビルドする方法があります。
remote build だとローカル環境に依存せずビルドできますが、pip のインストールサイズが大きくなったりするとエラーになることがあります。じゃぁ local build して解決しようって浅はかに思いそうですが、ローカルが Windows でビルド、動く環境は Azure Functions (Linux) で大丈夫かって問題が見えてきます。もう面倒になって Docker にするという手段もあります。Docker 楽そうだねってのはありますが docker image の管理も必要になります。リリースまでにひと手間かかる。あれ?CI/CD するなら結局 Linux で build できるからやっぱ image の管理が強いられる docker いらないよねって話になったり。こんな面倒な問題がない ONNX + C# でよくね、推論するだけならコード量はたかが知れてるしとかも出てくるでしょう。

メリットデメリットそれぞれ持っててケースバイケースなので、ガチでプロダクトを作るなら状況に合わせて最適なものを検討する必要があります。

Windows/Linux 環境差異の問題は WSL2 上で開発すれば解決するのかなぁと思っていますのでそのうち検証しようかなと。

個人的には実運用で Function App にわざわざ実行速度の遅い Python を使うとゆー選択をしてないんですよねー素直に C# で書くので。

まぁ入門編ではこんなこと気にせずに心のど真ん中において置き、次に進みます。

Azure ポータルで動作確認

Azure ポータルの Function App のリソースに戻りましょう。

ポータルで迷子になったら、左側のハブメニューでリソースグループ をクリック > 作成したリソースグループをクリックして Function App のアイコンのリソースをクリックしましょう。

関数 をクリックすると先ほどデプロイした HelloHttp の Function App が見えます。クリックしましょう。

f:id:beachside:20200813191257p:plain

コードとテスト の中に関数の URL 取得 をクリックすると、この Function App にアクセスする URL が取得できます。
(今更ポータルを日本語にした...)

f:id:beachside:20200813191848p:plain

これで、先ほどデバッグ時に試したのと同様にクエリパラメーターをつけて送信すると、動作することが確認できます。
全く触れませんでしたが、functions.json を見ると authLevelfunction になってます。これはアクセスするのにキーが必要というアクセスレベルなので、取得した URL には Function App が生成したキーがついています。name のクエリパラメーターは、URL の最後に &name=beachside とかな感じで付けます。

アクセスレベルについてはこちらのドキュメントに記載があります。

Application Insights でリアルタイムにログを見る

Azure ポータルで先ほど作成したリソースグループを見ると、Application Insights のリソースがありますのでクリックしましょう。

f:id:beachside:20200813212844p:plain

Live Metrics をクリックするとライブでログが見れます。Azure Functions が Sleep していると以下の画面のように「使用できません...」と表示されますが気にしなくて大丈夫です。

f:id:beachside:20200813213006p:plain

Function App を実行してみると以下のように動き出します (何度か実行して反応がなければ Live Metrics をもう一度クリックすると反応します) 。

f:id:beachside:20200813213232p:plain

これで、リアルタイムにログを見えます。また、メニュー: Live Metrics の下にある 検索 は数分遅れでログが入ってきて検索することができます。さらに下の方にあるログってメニューでより高度な検索や分析ができます。

おわりに

超入門的な位置づけとして HTTP Trigger での Function App をざっくり試しました。

Azure Functions の良さは、サーバーレスなので負荷が増えてもオートスケールしてくれるとかもありますが、ほかの Azure のサービスと連動してイベントドリブンで起動する便利な仕組み (Queue やファイルのアップロード・変更を検知して起動するとか) が用意されている点もあります。
Python だと ML やデータサイエンスでの用途だとデータの変更を検知して何か処理するとかをするのも容易なので、レガシーな日次バッチでの処理から、データ変更のイベントを検知して処理が動くアーキテクチャーへ進化を遂げるのに役立ちます。

参考

docs.microsoft.com

Azure Functions の Python 開発者向けリファレンス | Microsoft Docs

Azure Active Directory のテナントを削除する

いらなくなった Azure AD のテナントを削除する際、以下図のよう削除前にやるべきことを示唆されます。
ほとんどが画面上からポチポチして状態をグリーンにできるんですが、エンタープライズアプリケーションの削除は、画面上では削除できないやつがあります。イラっとしますね。

f:id:beachside:20200722141753p:plain:w600

Azure Portal からは削除できないエンタープライズアプリケーションを PowerShell を使って削除する方法のメモです。

PowerShell の準備

公式ドキュメント に記載がある通り、利用できるの PowerShell の 5.1 ~ 6、64 bit OS のみなので要注意です。

ドキュメントを疑って PowerShell 7 を Windows Terminal から起動して試しましたが、コマンドを実行してサインイン後に死にました)。

モジュールのインストール

PowerShell を管理者権限 で起動します。ちなみに私は Windows Terminal から PowerShell を実行しました。

今回使うコマンドは Azure Active Directory PowerShell for Graph (通称 AzureADv2) を使うので、インストールします。

Install-Module AzureAD

エンタープライズアプリケーションをぶっころ...

AAD のテナント ID を取得

Azure AD へのサインインをするのにテナントのテナント IDを使いたいので、Azure Portal で削除したい Azure AD にログインして、概要のメニューからテナント IDをコピーしておきます。

f:id:beachside:20200722145957p:plain:w480

モジュールの Import

では、モジュールをインポートするのに以下のコマンドを打ちます。そうするとなんか聞かれますがもちろん実行します。

Import-Module -Name AzureAD

f:id:beachside:20200722151536p:plain

エンタープライズアプリケーションの削除

ここからようやく本題です。引き続き PowerShell で以下のコマンドをたたきます。コマンドのオプションで前述で取得した AADの テナント ID を指定することでと想定外のテナントと接続してしまうリスクを避けます。

connect-azuread -tenantid <テナント ID>

サインインのウインドウが開きますのでサインインします (縦長やな...) 。

f:id:beachside:20200722150643p:plain:w300

サインインできたら、接続した情報が表示されます。TenantDomain が、削除したい AAD であることを確認しておきましょう。

f:id:beachside:20200722153012p:plain

次は Azure Portal で Azure AD を開く > テナントの削除 をクリック > (以下図の画面で) すべてのエンタープライズアプリケーションを削除するをクリックします。

f:id:beachside:20200722141753p:plain:w600

手で削除できなかったエンタープライズアプリケーションを開き、オブジェクト ID を確認しておきましょう。

で、PowerShell で Remove-AzureADServicePrincipal のコマンドを打ちます。オプションでオブジェクト ID を付けます。

Remove-AzureADServicePrincipal -ObjectId <オブジェクト ID>

f:id:beachside:20200722154227p:plain

Get-AzureADServicePrincipal のコマンドでサービスプリンシパルの一覧を取ってきて ForEachで片っ端から削除することができますが、私の場合 1つだけなのでやりません。やるならこんな感じのコード
$targetApps = Get-AzureADServicePrincipal
$targetApps.ForEach{ Remove-AzureADServicePrincipal -ObjectId $_.ObjectId}

Azure Portal でエンタープライズアプリケーションを見ると空っぽになりました!縦から見ても横から見てもリロードしてもログインしなおしてみても完全無欠の空っぽです ( ・ㅂ・)و ̑̑

f:id:beachside:20200722154755p:plain:w600

いざAzure AD のテナントを削除!........っておい ( 'д'⊂彡☆))Д´) パーン!!!!!!!!
エンタープライズアプリケーションをぶっころせてない?!

f:id:beachside:20200722155251p:plain:w600

と、上の画面は今日4つのディレクトリを削除なかで唯一削除できなかったテナントの話なのですが、ほかの3つのディレクトリは普通に削除できたし、以前にもこのやり方でたくさん削除してきたので手順は問題ないと思っています。
(根拠はありませんが、今までも削除はできてるしっていうあくまで個人的な感覚です)

f:id:beachside:20200722162825p:plain

ちなみに削除できなかったディレクトリは5年以上前に作った古いものなんですよねー(ほかのはここ1-3年くらいに作ったもの)。その辺もなんか関係あるのかな...知らんけど。。。
サービスプリンシパルの一覧から全部削除したろうかとも思いましたが、それだと原因がよくわからない状態になる気がするので試さず、原因わかったらまた追記しようと思います(放置しそう...。

参考