読者です 読者をやめる 読者になる 読者になる

451 Unavailable For Legal Reasons

とあるゲームエンジニアのブログです

LINE Bot APIはServer whitelistを無くした方が良いと思う

Azure FunctionとWebJobで、Azure Storage Queueを介して、LINE Bot APIのメッセージの収集と送信を行うサンプルを作成しました。

github.com

LINE Bot APIへリクエストするにはServer whitelistにIPアドレスを設定する必要があります。*1

おそらく同一MIDを使ったDDoS攻撃への対策だと思いますが、このwhitelistのせいでグローバルIPアドレスを固定できないPaaSが使えなくなってしまいます。 *2

DDoS攻撃への対策についてはwhitelistではなく、一定時間内に一定数を超える複数のIPアドレスから、同一MIDが正しいChannel IDとChannel Secretでリクエストしてきた場合、ブラックリストへ登録してリクエストを一定時間遮断するような実装にするのが良いと思います。*3

例えば下記のような方法があります。

  1. Redisに、MIDをKeyに含めて、下記のようなHashを一定時間のExpireでSetしておく。
  2. isBlockedをチェックしtrueなら遮断
  3. リクエスト元IPアドレスがipAddressと異なる場合、新たなIPアドレスをipAddressをSetしてaddressChangedCountをIncrement
  4. addressChangedCountが一定数を超えたらisBlockedをtrueにしてExpireを延長しておく
{
  "ipAddress": "192.168.1.1",
  "addressChangedCount": 0,
  "isBlocked": false
}

現状、whitelistチェックのためにデータストアへアクセスしているならば、処理コストをあまり増やさずにBOT開発者の利便性を向上させることができると思います。

ちなみにサンプルで作成した構成は下記の通りで

  1. Azure Functionでメッセージを収集しAzure Storage Queueへストア
  2. Azure WebJobでAzure Storage Queueをデキューして返信メッセージを生成しSending messages APIへPOST

この構成だと返信ジョブが失敗した場合に、一定回数のリトライも、リトライに失敗した場合のデータの保存もWebJob SDKが面倒見てくれます。*4

PaaSだと面白いBOTを思いついたら手軽に試せてスケールもしやすいので、LINE Bot APIの開発者の方にはServer whitelistの撤廃を検討頂けるとありがたいです。*5

*1:Callback URLをリクエストしてもらうのはServer whitelistの設定不要

*2:「Azure Web AppsはグローバルIPアドレスを固定にすることは可能ですか?」とMicrosoftに問い合わせてみたところ「まだその機能がありません。将来的にも組み込まれるかどうかもわからないようです。」とのこと

*3:弊社のゲームではBOT検出&遮断アルゴリズムの1つにリクエスト元IPアドレスを使ったものを実装している

*4:queueのアイテム名に「-poison」を付けて別queueに保存される

*5:トライアル版だからServer whitelistがあっただけだったなら杞憂

LINQ to BigQueryを使ってSankey Diagram(サンキーダイアグラム)を作成しよう

LINQ to BigQueryとは?

BigQueryをLINQで扱えるようにするためのライブラリです。

github.com

どうやってBigQueryにデータを入れるの?

Sankey Diagramに必要な形式のデータを作成してBigQueryへ保存するためのOwinMiddleware

gist.github.com *1

StartActionに宣言したRequest.Pathへアクセスされるとログ採取スタート。 LoggingSpanに宣言した期間離脱するとキャッシュが無くなって採取おわり。 期間内にアクセスするとIndexを増やしながらFromPathとToPathをBigQueryへログ出力していく。

BigQueryに入れたデータをどうやって可視化するの?

LINQ to BigQueryのLINQ登場

gist.github.com *2

スキャン対象となるデータ量を抑えるために、BigQueryのテーブル名にはyyyyMMを付与しています。

Viewから呼び出し

gist.github.com *3

サンプルコードではutil.getJSONがAPI呼び出しをしていますが、その奥にあるのはjQueryのajaxメソッドです。

jQueryのajaxメソッドを呼び出すようにサンプルコードを修正しました。

完成!

f:id:master-0717:20160224215832p:plain

色々とAPIのパスが見えているので画像はぼやけさせていますが、Sankey Diagram(サンキーダイアグラム)感は、感じて頂けるのではないでしょうか。

改善

今回のサンプルコードでは、OwinContextにログインユーザーのIDしか入っていないパターンだったので、期間とステップ数でしか対象者を絞れませんでした。他にもユーザーの属性(例えばゲームならレベルとか)をBigQueryへ保存し閲覧する時に絞り込めれば、目的に合わせて分析できてなお良いのではないでしょうか。

GoogleのCLOUD VISION APIを使ってみる

昨日「Microsoft Project OxfordのComputer Vision APIを使ってみる」という記事を書きました。

blog.game-programmer.jp

Google Cloud PlatformにもVision APIがあるので試してみようと思います。

cloud.google.com

Visual StudioでWeb APIプロジェクトを作成。「Microsoft Project OxfordのComputer Vision APIを使ってみる」の時に作成したプロジェクトを使いまわします。 github.com

NuGet Gallery | Google.Apis.Vision.v1 Client Library 1.10.0.20 をインストール f:id:master-0717:20160223170851p:plain

APIを作成。今回はGoogle Cloud Storageへ 「エンジニアなんだからFAXも直せるでしょ」と言われる|フリー写真素材・無料ダウンロード-ぱくたそ のお姉さんの画像をアップロードして使用。全機能リクエストしてます。

結果はこちら。JSONが大きいのでスクリーンショットではなくテキストで。

{  
  responses:[  
    {  
      error:null,
      faceAnnotations:[  
        {  
          angerLikelihood:"VERY_UNLIKELY",
          blurredLikelihood:"VERY_UNLIKELY",
          boundingPoly:{  
            vertices:[  
              {  
                x:353,
                y:34,
                eTag:null
              },
              {  
                x:664,
                y:34,
                eTag:null
              },
              {  
                x:664,
                y:395,
                eTag:null
              },
              {  
                x:353,
                y:395,
                eTag:null
              }
            ],
            eTag:null
          },
          detectionConfidence:0.8755698,
          fdBoundingPoly:{  
            vertices:[  
              {  
                x:418,
                y:114,
                eTag:null
              },
              {  
                x:642,
                y:114,
                eTag:null
              },
              {  
                x:642,
                y:338,
                eTag:null
              },
              {  
                x:418,
                y:338,
                eTag:null
              }
            ],
            eTag:null
          },
          headwearLikelihood:"VERY_UNLIKELY",
          joyLikelihood:"VERY_UNLIKELY",
          landmarkingConfidence:0.592607558,
          landmarks:[  
            {  
              position:{  
                x:541.064636,
                y:175.69809,
                z:0.00113653147,
                eTag:null
              },
              type:"LEFT_EYE",
              eTag:null
            },
            {  
              position:{  
                x:606.808533,
                y:206.526047,
                z:62.9831734,
                eTag:null
              },
              type:"RIGHT_EYE",
              eTag:null
            },
            {  
              position:{  
                x:516.7551,
                y:147.708527,
                z:-8.272155,
                eTag:null
              },
              type:"LEFT_OF_LEFT_EYEBROW",
              eTag:null
            },
            {  
              position:{  
                x:574.37146,
                y:159.893372,
                z:10.4143019,
                eTag:null
              },
              type:"RIGHT_OF_LEFT_EYEBROW",
              eTag:null
            },
            {  
              position:{  
                x:606.229858,
                y:173.236755,
                z:40.98865,
                eTag:null
              },
              type:"LEFT_OF_RIGHT_EYEBROW",
              eTag:null
            },
            {  
              position:{  
                x:625.407654,
                y:192.494385,
                z:96.7955,
                eTag:null
              },
              type:"RIGHT_OF_RIGHT_EYEBROW",
              eTag:null
            },
            {  
              position:{  
                x:588.6517,
                y:183.960876,
                z:19.3756,
                eTag:null
              },
              type:"MIDPOINT_BETWEEN_EYES",
              eTag:null
            },
            {  
              position:{  
                x:599.7611,
                y:235.518936,
                z:-15.2664413,
                eTag:null
              },
              type:"NOSE_TIP",
              eTag:null
            },
            {  
              position:{  
                x:580.7478,
                y:269.287048,
                z:-9.681131,
                eTag:null
              },
              type:"UPPER_LIP",
              eTag:null
            },
            {  
              position:{  
                x:572.713135,
                y:300.852,
                z:-14.1530352,
                eTag:null
              },
              type:"LOWER_LIP",
              eTag:null
            },
            {  
              position:{  
                x:544.012451,
                y:279.2989,
                z:-21.0170631,
                eTag:null
              },
              type:"MOUTH_LEFT",
              eTag:null
            },
            {  
              position:{  
                x:588.0662,
                y:295.959778,
                z:28.44663,
                eTag:null
              },
              type:"MOUTH_RIGHT",
              eTag:null
            },
            {  
              position:{  
                x:574.9631,
                y:282.396027,
                z:-8.923364,
                eTag:null
              },
              type:"MOUTH_CENTER",
              eTag:null
            },
            {  
              position:{  
                x:593.7063,
                y:252.415634,
                z:24.9813824,
                eTag:null
              },
              type:"NOSE_BOTTOM_RIGHT",
              eTag:null
            },
            {  
              position:{  
                x:559.2501,
                y:241.225586,
                z:-9.623048,
                eTag:null
              },
              type:"NOSE_BOTTOM_LEFT",
              eTag:null
            },
            {  
              position:{  
                x:584.410034,
                y:250.873962,
                z:-4.506706,
                eTag:null
              },
              type:"NOSE_BOTTOM_CENTER",
              eTag:null
            },
            {  
              position:{  
                x:547.282837,
                y:170.129669,
                z:-2.050267,
                eTag:null
              },
              type:"LEFT_EYE_TOP_BOUNDARY",
              eTag:null
            },
            {  
              position:{  
                x:557.2416,
                y:182.196564,
                z:13.7818327,
                eTag:null
              },
              type:"LEFT_EYE_RIGHT_CORNER",
              eTag:null
            },
            {  
              position:{  
                x:540.6396,
                y:183.45163,
                z:-2.0629003,
                eTag:null
              },
              type:"LEFT_EYE_BOTTOM_BOUNDARY",
              eTag:null
            },
            {  
              position:{  
                x:522.974548,
                y:173.02684,
                z:-6.10401154,
                eTag:null
              },
              type:"LEFT_EYE_LEFT_CORNER",
              eTag:null
            },
            {  
              position:{  
                x:542.6806,
                y:176.329376,
                z:-2.00644469,
                eTag:null
              },
              type:"LEFT_EYE_PUPIL",
              eTag:null
            },
            {  
              position:{  
                x:612.3812,
                y:197.071289,
                z:60.93033,
                eTag:null
              },
              type:"RIGHT_EYE_TOP_BOUNDARY",
              eTag:null
            },
            {  
              position:{  
                x:613.783142,
                y:210.560913,
                z:81.82983,
                eTag:null
              },
              type:"RIGHT_EYE_RIGHT_CORNER",
              eTag:null
            },
            {  
              position:{  
                x:606.9622,
                y:210.3664,
                z:61.05775,
                eTag:null
              },
              type:"RIGHT_EYE_BOTTOM_BOUNDARY",
              eTag:null
            },
            {  
              position:{  
                x:593.684265,
                y:199.262146,
                z:50.88671,
                eTag:null
              },
              type:"RIGHT_EYE_LEFT_CORNER",
              eTag:null
            },
            {  
              position:{  
                x:609.48584,
                y:203.937683,
                z:62.56031,
                eTag:null
              },
              type:"RIGHT_EYE_PUPIL",
              eTag:null
            },
            {  
              position:{  
                x:551.33905,
                y:142.170044,
                z:-0.1636725,
                eTag:null
              },
              type:"LEFT_EYEBROW_UPPER_MIDPOINT",
              eTag:null
            },
            {  
              position:{  
                x:621.85144,
                y:171.3207,
                z:68.1858139,
                eTag:null
              },
              type:"RIGHT_EYEBROW_UPPER_MIDPOINT",
              eTag:null
            },
            {  
              position:{  
                x:412.211426,
                y:214.9295,
                z:28.4080238,
                eTag:null
              },
              type:"LEFT_EAR_TRAGION",
              eTag:null
            },
            {  
              position:{  
                x:559.8016,
                y:278.0976,
                z:170.527115,
                eTag:null
              },
              type:"RIGHT_EAR_TRAGION",
              eTag:null
            },
            {  
              position:{  
                x:593.098938,
                y:165.201691,
                z:23.6455746,
                eTag:null
              },
              type:"FOREHEAD_GLABELLA",
              eTag:null
            },
            {  
              position:{  
                x:560.7295,
                y:341.240265,
                z:-17.0568733,
                eTag:null
              },
              type:"CHIN_GNATHION",
              eTag:null
            },
            {  
              position:{  
                x:440.485,
                y:275.949,
                z:-9.309473,
                eTag:null
              },
              type:"CHIN_LEFT_GONION",
              eTag:null
            },
            {  
              position:{  
                x:573.0591,
                y:328.770172,
                z:119.69677,
                eTag:null
              },
              type:"CHIN_RIGHT_GONION",
              eTag:null
            }
          ],
          panAngle:44.0354538,
          rollAngle:7.173631,
          sorrowLikelihood:"VERY_UNLIKELY",
          surpriseLikelihood:"VERY_UNLIKELY",
          tiltAngle:16.4116535,
          underExposedLikelihood:"VERY_UNLIKELY",
          eTag:null
        }
      ],
      imagePropertiesAnnotation:{  
        dominantColors:{  
          colors:[  
            {  
              color:{  
                alpha:null,
                blue:110,
                green:84,
                red:70,
                eTag:null
              },
              pixelFraction:0.107155956,
              score:0.173213258,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:235,
                green:231,
                red:228,
                eTag:null
              },
              pixelFraction:0.2768196,
              score:0.155500337,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:21,
                green:22,
                red:25,
                eTag:null
              },
              pixelFraction:0.08605505,
              score:0.0613242574,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:99,
                green:112,
                red:145,
                eTag:null
              },
              pixelFraction:0.00281345565,
              score:0.00267118076,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:143,
                green:109,
                red:90,
                eTag:null
              },
              pixelFraction:0.08672783,
              score:0.138454765,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:209,
                green:201,
                red:191,
                eTag:null
              },
              pixelFraction:0.121957183,
              score:0.106325813,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:69,
                green:26,
                red:null,
                eTag:null
              },
              pixelFraction:0.0002446483,
              score:0.0006086802,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:81,
                green:60,
                red:46,
                eTag:null
              },
              pixelFraction:0.0321712531,
              score:0.0508035943,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:54,
                green:52,
                red:53,
                eTag:null
              },
              pixelFraction:0.05877676,
              score:0.0491721,
              eTag:null
            },
            {  
              color:{  
                alpha:null,
                blue:87,
                green:78,
                red:74,
                eTag:null
              },
              pixelFraction:0.0503975525,
              score:0.0448334664,
              eTag:null
            }
          ],
          eTag:null
        },
        eTag:null
      },
      labelAnnotations:[  
        {  
          boundingPoly:null,
          confidence:null,
          description:"disc jockey",
          locale:null,
          locations:null,
          mid:"/m/02dsz",
          properties:null,
          score:0.904773355,
          topicality:null,
          eTag:null
        },
        {  
          boundingPoly:null,
          confidence:null,
          description:"profession",
          locale:null,
          locations:null,
          mid:"/m/063km",
          properties:null,
          score:0.8840673,
          topicality:null,
          eTag:null
        },
        {  
          boundingPoly:null,
          confidence:null,
          description:"person",
          locale:null,
          locations:null,
          mid:"/m/01g317",
          properties:null,
          score:0.8571039,
          topicality:null,
          eTag:null
        }
      ],
      landmarkAnnotations:null,
      logoAnnotations:null,
      safeSearchAnnotation:{  
        adult:"VERY_UNLIKELY",
        medical:"VERY_UNLIKELY",
        spoof:"VERY_UNLIKELY",
        violence:"VERY_UNLIKELY",
        eTag:null
      },
      textAnnotations:[  
        {  
          boundingPoly:{  
            vertices:[  
              {  
                x:1039,
                y:818,
                eTag:null
              },
              {  
                x:1089,
                y:818,
                eTag:null
              },
              {  
                x:1089,
                y:844,
                eTag:null
              },
              {  
                x:1039,
                y:844,
                eTag:null
              }
            ],
            eTag:null
          },
          confidence:null,
          description:"JusTio ",
          locale:"fr",
          locations:null,
          mid:null,
          properties:null,
          score:null,
          topicality:null,
          eTag:null
        }
      ],
      eTag:null
    }
  ],
  eTag:null
}

お姉さんの表情、顔の各パーツの位置、画像のラベル付け、コピー機に書かれている「JusTio」という文字列まで様々な情報が取り出せました。

GoogleのCLOUD VISION APIはバッチでまとめて画像のメタデータを作成しておいて、後からそれを使用するのに向いているかもしれませんね。

Application Insightsで本番運用時に気を付けること

Application Insightsとは?

Microsoft Azureのアプリケーション監視サービスです。 azure.microsoft.com

本番運用で考慮すべき「データ速度」

azure.microsoft.com

月間クォータの他にも、データ速度でのスロットルの上限もあります。上限は、Free 価格レベルでは、5 分間の平均が 1 秒あたり 200 データ ポイント、有料レベルでは、1 分間の平均が 1 秒あたり 500 データ ポイントになります。

とあるゲームアプリをApplication Insightsで監視しているのですが、初期にデフォルト設定で本番投入したらスロットルが発動し続けました。

こんなメッセージがAzureポータルに表示され続けます。 f:id:master-0717:20160223125946p:plain

スロットルが発動するということは不要にデータ送信していることになっていてネットワーク帯域がもったいないし、ポータルにすごい頻度でメッセージが表示され続けるのは辛いです。

そこで「Application Insights の価格とクォータの管理」に記載されている下記の2つを実施したところ、スロットルが発動しなくなりました。

記載内容
  • サンプリングの使用。このテクノロジは、メトリックや、検索で関連するアイテム間を移動する機能を損なうことなく、データ レートを削減します。
  • ApplicationInsights.config を編集し、不要なコレクション モジュールを無効にします。たとえば、パフォーマンス カウンターや依存関係のデータが重要ではないと判断した場合などに検討します。
具体的には
  • TelemetryModulesは下記の2つのみに

    • RequestTrackingTelemetryModule
    • ExceptionTrackingTelemetryModule
  • RequestTelemetryによるサンプリングを全リクエストの1%に

本番で設定しているアラート

現状、下記の3項目です。

  • 直近5分以内にレスポンスタイムが3秒以上
  • 直近5分以内にExceptionが30回以上
  • 2つのロケーションからの可用性テスト失敗

f:id:master-0717:20160223130802p:plain

Application InsightsにはProactive Detectionという、機械学習およびデータ マイニングのアルゴリズムを使用して異常パターンを検出してくれるサービスがあるので、レスポンスタイムやException数ぐらいなら監視項目設定の必要が無いかもしれません。

blog.shibayan.jp

Azure App Serviceと組み合わせて使う場合はInstrumentationKeyをWeb.configへ

Azure App ServiceはappSettingsをスロット毎に上書きできるので、InstrumentationKeyをWeb.configへ定義してApplication_Startで設定しておくと便利です。

azure.microsoft.com

protected void Application_Start()
{
  Microsoft.ApplicationInsights.Extensibility.
    TelemetryConfiguration.Active.InstrumentationKey = 
      // - for example -
      WebConfigurationManager.Settings["ikey"];
  //...
まとめ

Application Insights便利ですが、データ送信量を間引くのはconfigファイルやTelemetryConfigurationクラスのプロパティへの設定で簡単に出来てほしいところ。DependencyCollectorなんかも間引ければ本番で使用したいです。

Microsoft Project OxfordのComputer Vision APIを使ってみる

Microsoft Project Oxford

Microsoft Project Oxfordって?

Microsoft Project Oxford Home は、機械学習関連のAPIをいくつか公開しているプロジェクトです。

jp.techcrunch.com

APIへアクセスするには?

プロジェクトサイトの「Get started for free today」リンクを踏むとMicrosoftアカウント*1へのアクセス許可ページに飛ぶので、許可するとアクセスキーが発行できるようになります。

Computer Vision APIを使ってみる

Visual Studio*2でプロジェクトを作成 f:id:master-0717:20160222231603p:plain

f:id:master-0717:20160222231643p:plain

ProjectOxford-ClientSDK/Vision at master · Microsoft/ProjectOxford-ClientSDK · GitHub のNuGetパッケージをインストール

f:id:master-0717:20160222231653p:plain

APIを作成。Streamを引数に渡して分析することも可能です。

「エンジニアなんだからFAXも直せるでしょ」お姉さんの画像を分析してみる。

www.pakutaso.com

結果はこちら*3

f:id:master-0717:20160222235429p:plain

アダルトコンテンツかどうか、顔の位置、性別、年齢(14歳になっちゃってますが)等が返ってきます。

APIの利用料は?

Microsoft Project Oxford Pricing に書いてありますが、現在プレビュー価格で5,000トランザクションまで無料、10トランザクション毎秒を上限に1000トランザクションあたり1.5ドルで使えるようです。

何に使えそう?

自由に画像アップロードできるサービスで公開して良い画像かどうかの判断等に使えそうですね。今後の精度アップやプロパティ追加に期待です。

*1:アカウントを持っていない人は要作成

Microsoft アカウント登録手続き|Microsoft アカウント

*2:Visual Studioを持っていない人は要インストール

Visual Studio Community - Visual Studio

*3:JSONViewを使って表示しています。

JSONView - Chrome Web Store