銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

【読み始めました】 「Pro Unity Game Development with C#」

Unity Advent Calendar の10日目のエントリ...だったのですが、いろいろ体調不良が重なり当初書評を書く予定で読んでいた本が読み切れませんでした...というわけなので、なぜこの本を読もうとしたのかと、読んだところまでの感想を書きたいと思います。

今回読んだ本は「Pro Unity Game Development with C#」です。シアトルのMS本社内のMicrosoft Storeに行った際に割引していたこともあり紙の本で購入しました。

読もうとしたきっかけ

Unity なんですが、会社としては Unity による開発プロジェクトが進行しており、Unity以外のプロジェクトに携わるエンジニアも含めて全員がUnityの勉強を定期的にしています。勉強会ではテーマを設けて丸一日そのテーマについて勉強するので、そのテーマの周辺の理解を深めることはできるのですが、そもそもゲーム全体をUnityでどうやってつくるのか?という全貌はなかなか見えてきません。そこでUnityによる全体像を学ぶために本で勉強しようと思いました。

本の概要

この本は章に沿って、Unityプロジェクトを開発していき、最終的に1つのゲームを作る形式になっています。途中まで読んだ感想としては、丁寧に解説していると思います。Unity そのものの操作もそうですし、途中で記述する C# コードについてもそうです。また、コードをダウンロードできるので、ちょっと書いていってこれであっているかな?と思ったときに答え合わせすることもできます。

という感じで非常に短くて申し訳ないのですが、いったんこれで本日分のエントリにします。読破して、プロジェクトを作った時に改めて書評を書きたいと思います。

ASP.NET/IIS 上で X509Certificate2 をバイト配列指定で生成するときは、 Application Pool の実行ユーザーのプロファイルを読みこませないといけません

C# Advent Calendar と ASP.NET Advent Calendar の9日目のエントリです。

もともと別の内容を書くつもりだったのですが、仕事であまりにはまったことがあったので、それを書こうと思います。

背景

Google API をたたきたい状況で、APIサーバー側(つまりGoogle側)で発行される証明書を使って、 X509Certificates2 クラスのインスタンスを生成して認証する必要があります。

X509Certificate2 クラス (System.Security.Cryptography.X509Certificates)

証明書ファイルのパスを指定してインスタンスを生成することもできますが、

X509Certificate2 コンストラクター (String, String, X509KeyStorageFlags) (System.Security.Cryptography.X509Certificates)

証明書ファイルのコンテンツを byte配列として渡すこともできます。

X509Certificate2 コンストラクター (Byte[], String, X509KeyStorageFlags) (System.Security.Cryptography.X509Certificates)

どういうときにbyte配列を指定したいかというと、複数のWebサーバーで同じ証明書を使うときなどにネットワーク上のストレージに証明書を置いて管理したいときになります*1

ネットワーク越しに証明を取得するコード

AWS SDK を使って、S3から証明書を取得してX509Certificates2 クラスのインスタンスを生成するコードはこんな感じになります。

var s3 = AWSClientFactory.CreateAmazonS3Client(s3Key, s3Secret, RegionEndpoint.APNortheast1);
var s3Obj = s3.GetObject(certficateBucket, certificateKey);

byte[] bytes = null;
using (var responseStream = s3Obj.ResponseStream)
{
    bytes = responseStream.ReadFully();
}

var certificate = new X509Certificate2(bytes, certificatePassword, X509KeyStorageFlags.Exportable);

//以下の拡張メソッドを用意
public static class StreamExtension
{
    public static byte[] ReadFully(this Stream input)
    {
        var buffer = new byte[16 * 1024];
        using (var ms = new MemoryStream())
        {
            int read;
            while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
            {
                ms.Write(buffer, 0, read);
            }
            return ms.ToArray();
        }
    }
}

このようなコードを Global.asax.csApplication_Start メソッドの中から呼び出していました。

起きたこと

で、このコードを載せたアプリをIISで動かすとエラーが発生しました。(以下のエラーはエラーの詳細を表示する設定を Web.config に指定している場合にブラウザ上に表示されるメッセージの一部)

[CryptographicException: 指定されたファイルが見つかりません。]
   System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr) +41
   System.Security.Cryptography.X509Certificates.X509Utils._LoadCertFromBlob(Byte[] rawData, IntPtr password, UInt32 dwFlags, Boolean persistKeySet, SafeCertContextHandle& pCertCtx) +0
   System.Security.Cryptography.X509Certificates.X509Certificate.LoadCertificateFromBlob(Byte[] rawData, Object password, X509KeyStorageFlags keyStorageFlags) +313
   System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(Byte[] rawData, String password, X509KeyStorageFlags keyStorageFlags) +98

このエラー、ブラウザ上に表示しないようにしていると、イベントログにこんな感じに表示されるだけで、さらに謎になります*2

(一部抜粋)

障害が発生しているアプリケーション名: w3wp.exe、バージョン: 8.0.9200.16384
障害が発生しているモジュール名: ntdll.dll、バージョン: 6.2.9200.16579
例外コード: 0xc0000374
障害オフセット: 0x00000000000ebd59

障害が発生しているアプリケーション パス: c:\windows\system32\inetsrv\w3wp.exe
障害が発生しているモジュール パス: C:\Windows\SYSTEM32\ntdll.dll

障害が発生しているパッケージの完全な名前: 
障害が発生しているパッケージに関連するアプリケーション ID: 

なぜ、 byte配列を指定しているのにファイルが見つからないといわれるのか...

原因と対策

いろいろぐぐっていたら似たようなことにはまった人がStackOverflowにいて自己解決していました。

どうやら、X509Certificate2 を byte配列を指定してインスタンス化しようとすると内部でユーザープロファイルに書きこみ処理を行っているようで、IIS でApplication Poolの実行ユーザーに対してユーザープロファイルの読みこみを指定していないと書きこむことができなくてエラーになるようです。 というわけで、ユーザープロファイルを読みこむようにしてみましょう。以下のサイトがわかりやすいです。

ApplicationHost.config で次のように設定すればOKです(他のIdentityType でもいけるはず)。

<system.applicationHost>
    <applicationPools>
        <add name="MyAppPool">
            <processModel identityType="SpecificUser" userName="MyAppPoolUser" password="password" />
        </add>
    </applicationPools>
    <!--省略-->
</system.applicationHost>

この設定をすると無事最初のコードで X509Certificates2をインスタンス化して、認証に使うことができました。めでたし、めでたし。 なお、ユーザープロファイルのディレクトリを見ても、この証明書ファイルとして保存しているわけではないようです。

なお、もともとC# Advent Calendarで書く予定だった社内ライブライのビルド・デプロイ戦略については改めて書きたいと思っています。

*1:S3に証明書を置いて、その証明書を取得できる権限を IAM Role で制御するなど

*2:例外コードでぐぐったり、VSでプロセスにアタッチしたりすると「ハンドルされない例外が 0x000007FD14B4BD59 (ntdll.dll) で発生しました(w3wp.exe 内): 0xC0000374: ヒープは壊れています。 (パラメーター: 0x000007FD14BA05F0)。」とかいうメッセージが出るのもさらに謎を深める

.NET アプリから BigQuery に Streaming Insert する方法

このブログは Google Cloud Platform Advent Calendar 2014 の7日目のエントリです。

Google Cloud Platform Advent Calendar 2014 - Qiita

さて、以前テーブルをAPIから生成する方法は紹介したのですが、データをStreaming Insertする方法について書いていなかったので、そこを説明したいと思います。また、すでに本番運用して、WebサーバーからデータをBigQueryに送信しているのですが、どのような点に注意しているかも紹介したいと思います。

C# から Streaming Insertする方法

まずはサンプルコードをのせます。

StreamingInsert.cs

CreateTable メソッドは前回の記事を参考にしてください。LoadRowsメソッドは実際に送信するデータですが、でたらめな値を入れるのであればこんな感じになります。特徴としては、テーブル定義に対応した列名とその値を Dictionary<string,object> で指定することです。

private IList<TableDataInsertAllRequest.RowsData> LoadRows()
{
    return Enumerable.Range(0, 450)
        .Select(_ => new TableDataInsertAllRequest.RowsData
        {
            InsertId = Guid.NewGuid().ToString(), //リクエストの一意なIdなのでGuidで代用
            Json = new Dictionary<string, object>
            {
                { "date", DateTime.Now },
                { "source", "hoge" }
            }
        }).ToList();
}

長くなっていますが、基本的には

var req = new TableDataInsertAllRequest
{
    Rows = LoadRows()
};

var response = await bigquery.Tabledata.InsertAll(req,
                    "testprj", "testdataset", "testtbl").ExecuteAsync();

でInsertできます。面倒なことをしているのは、

  • Insert先のテーブルがないときにテーブルを生成する処理
  • 失敗時にリトライする処理

を入れているからです。

テーブルの自動生成処理を入れるかどうかはInsertの要件次第でしょう。たとえば期間ごとにテーブルを分けて格納する場合*1に、新しいテーブルを作るたタイミングで別のツールから生成するのではなく、このコードの中で生成することを想定しています*2

また、後者の失敗時のリトライですが、これは割と必須の処理です。BigQuery のStreaming InsertはバックエンドのエラーでInsertが失敗することがあり、その場合にはリトライすることが推奨されています。リトライも単純に一定時間おきに繰り返すのではなく、指数関数的な伸び+ランダムに追加した時間だけ待機する Google.Apis.Util.ExponentialBackOff を利用しています。なお、所定の最高試行回数(ExponentialBackOff の初期値だと10)を超えると、Insertをあきらめデータは破棄することになります。

本番運用で考慮していること

というわけで上のような Streaming Insert なコードを本番環境で動かして BigQuery にデータを送信しています*3

さて、そもそもどうやってこのコードを動かしているかですが、 上のコードを SLABのSink として実装し、 Out-of-Process Service として動かしています。送信するイベントを送信するのはIIS上のASP.NETアプリになります。

テーブルの自動生成と失敗時のリトライは上のコードのように考慮していますが、ほかに次のようなことを考慮して動かしています。

  • Insert先のDataSet名、Table名、テーブル定義などは EventSource のメタ情報として定義しています。 DataSet名 は EventKeyword、Table名は EventTask、テーブル定義は Paylowd 経由で取得、という感じです。
  • 1回のリクエストあたり500行というハードリミットがあるので、450行ずつバッファリングして送信
  • SLABのSinkでは BufferedEventPublisher<EventEntry> でイベントをバッファリングしてまとめて送信できるが、バッファが溜まった時に発火するeventPublisher*4の中でリトライを繰り返すと、後続のログもたまり続けるため、1回失敗した場合は、別の BufferedEventPublisher にためていくようにしている
  • Insertしている Out-of-Process Service の健全性は、Windows EventLogおよびパフォーマンスカウンタを通して確認している。*5

といった点です。

というわけで、意外な組み合わせかもしれませんが、.NET アプリからもBigQueryにデータを送信して活用しています、というお話でした。

*1:test_20141201, test_20141202 ... みたいなテーブル名を想定

*2:その場合、テーブル名は日時に応じて動的に判断することになり、テーブル構造を指定する必要もある

*3:動いているコードそのものをお見せしていないのは、テーブル名やテーブル定義の生成部分に弊社固有のロジックが入ってコードの見通しが悪いためです...

*4:CreateAndStartメソッドの第2引数で指定

*5:実際はそれらを AWS CloudWatch で集約してアラートをしかけている