True Cloud Native Batch Workflow for .NET with MicroBatchFramework

AWS .NET Developer User Group 勉強会 #1にて、先日リリースしたMicroBatchFrameworkについて、話してきました。

True Cloud Native Batch Workflow for .NET with MicroBatchFramework from Yoshifumi Kawai

タイトルが英語的に怪しいですが、まぁいいでしょう(よくない)

MicroBatchFrameworkの概要については、リリース時にCygames Engineers' BlogにてMicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワークとして書かせていただきました。そう、最近はそっち(どっち)に書いてしまうのでこっち(あっち)に書かれなくなる傾向が!リポジトリの置き場としても、Cysharpオーガナイゼーション中心になってきています。これは会社としてメンテナンス体制とかもしっかり整えていくぞ、の現れなので基本的にはいいことです。

ちなみにCysharp、ページ下段にお問い合わせフォームが(ついに)付きました。興味ある方は応募していただいてもよろしくてよ?ビジネスのお問い合わせも歓迎です。別にゲームに限らずで.NET Coreの支援とかでもいいですよ。ただしオールドレガシーWindows案件はやりません。

クラウドネイティブ

これはセッションで口頭で言いましたが、バズワードだから付けてます。という側面は大いにあります。世の中マーケティングなのでしょーがないね。そもそも私はそういうのに乗っかるの、好きです。

そんな中身のないクラウドネイティブですが(真面目な定義はCNCFのDefinitionにちゃんとあります)、まぁコンテナ化です。ベンダー中立な。というのをコンテナ化ビリティの高さという表現に落としました。.NET Coreは結構いい線言ってると思いますよ。実際。

さて、そんなクラウドネイティブなふいんきのところでの、理想のバッチ処理ってなんやねん。というのを考えて、逆算でアプリケーション側で埋めるべきものを埋めるために作ったのがMicroBatchFrameworkです。インフラ側の欠けてるところはそのうちクラウド事業者が埋めてくれるか、現状でも全然実用レベルで回避はどうとでもなるでしょう。

私としてはC#が快適にかければなんだっていいんですが、なんだっていいというだけではなくC#としての自由の追求に関しては相当ラディカルなのですが、でも、それって割とクラウドネイティブの定義(ちゃんとしたほうの)通りなんですよね。別にコンテナに夢見てるわけじゃなくて、意外と堅実に正しく定義どおりのことやってるわけです。まー、FaaSのオーケストレーターは私の理想からベクトル真逆だし、FaaSのランタイムの重さ(実行が遅いという意味ではなくてシステムとしてのヘヴィさ)も受け入れ難いんで、世の中の正しい進化について正面から向かい合うのが結局一番ということで。

ところでMicroBatchFrameworkのウェブホスティング機能(MicroBatchFramework.WebHosting)はSwaggerによる実行可能なドキュメント生成、のほかに、HTTPをトリガーにする待ち受けという側面もあります。GCP Cloud Runの実行のためにはそういうの必要ですからね。毎回コンテナ起動みたいな夢見たモデルだけじゃなくて、割とちゃんと現実に即して機能は用意してます。意外と。割とちゃんと。そもそも、その辺は実用主義なので。

MicroBatchFrameworkはいい具合のバランス感覚で作れていると思うので、実際良いと思います。というわけで、是非試していただければですね。

CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまで

死ぬほどお久しぶりです!別にインターネット的には沈黙してるわけじゃなくTwitterにもいるし、会社(Cysharp)関連で露出あるかもないかもというわけで、決して沈黙していたわけでもないはずですが、しかしブログは完全に放置していました、あらあら。

C#的にも色々やっていて、CloudStructuresのv2を@xin9leさんとともにリリースしたり、多分、今日に詳細を書くつもりですがMicroBatchFrameworkというライブラリをリリースしたり、Ulidというライブラリをリリースしてたり、まぁ色々やってます。ちゃんと。実際。今月はそのMicroBatchFramework関連で、AWS .NET Developer User Group 勉強会 #1に登壇しますし。リブートしたMagicOnionも来月勉強会開催予定だったりで、めっちゃやる気です。

さて、そんなやる気に満ち溢れている私なのですが(実際Cysharpもいい感じに動き出せているので!お問い合わせフォームないけどお問い合わせ絶賛募集中!)、ブログは放置。よくないね。というわけで表題の件について。

目的と目標

CIの有効性について未だに言う必要なんてなにもないわけですが、しかし、.unitypackageを手作業で作っていたのです。今まで。私は。UniRxとかMessagePack-CSharpの。そして死ぬほど面倒くさいがゆえに更新もリリースも億劫になるという泥沼にハマったのです。やる気が満ち溢れている時は手作業でもやれるけれど、やる気が低下している時でも継続してリリースできなければならないし、そのためにCIはきっちりセットアップしておかなければならないのです。という真理にようやく至りました。なんで今さらなのかというと、私がアプリケーション書くマンであることと、CIとかそういうのは全部、部下に丸投げして自分は一切手を付けてこなかったマンだからです。しかし会社のこともあるので、いい加減にそれで済まなくなってきたので(今更やっとようやく)真面目に勉強しだしたのですね……!

で、CIにはCircleCIを使います。なんでCircleCIなのかというと、一つはUnity Cloud Buildだとunitypackageを作れない(多分)というのが一つ。もう一つは、私が.NET CoreのCIもCircleCIに寄せているので、統一して扱えるといいよねというところです。また、Linuxの他にMacでのビルドもできるので(有料プラン)、iOSに、とかも可能になってくるかもしれませんしね。あと、単純にCircleCIが昨今のCIサービスで王者なので、長いものに巻かれろ理論でもある。でも私自身も最近使っていてかなり気に入ってるので、実際良いかと良いかと。コンテナベースで記述するのがとても小気味よいわけです、モダンっぽいし。

ゴールは

  • リポジトリの一部ソース郡から.unitypackageを作る
  • EditorでUnitTestを行う
  • IL2CPP/Windowsでビルドする(↑のUnitTestのIL2CPP版を吐く)

となります。普通はAndroidやiOSビルドがしたいって話だと思うのですが、私はライブラリAuthorなので、まずそっちの要求のほうが先ということで(そのうちやりたいですけどね!)。Editorテストだけじゃなくて、IL2CPPで動作するか不安度もあるので、そっちのexeも吐ければ嬉しい。できればIL2CPPビルドのものも、ヘッドレスで起動して結果レポーティングまでやれればいいん&ちょっと作りこめばそこまで行けそうですが、とりあえずのゴールはビルドして生成物を保存するところまでにしておきましょう。そこまで書いてると記事長くなるし。

認証を通してUnityをCircleCI上で動かす

CircleCIということでコンテナで動かすんですが、まぁUnityのイメージを持ってきてbatchmodeで起動して成果を取り出すという、それだけの話です。適当にUnityのコマンドライン引数とにらめっこすれば良い、と。

コンテナイメージに関しては、幸い誰か(gablerouxさん)がgableroux/unity3d/tagsに公開してくれていて、綺麗にタグを振ってくれています。コンテナの良いところっていっぱいあると思いますが、コンテナレジストリが良い具合に抽象化されたファイル置き場として機能するのも素敵なとこですねえ。また、こうして公開してくれていれば、社内CIのUnityインストール管理とかしないで済むのも良いところです。大変よろしい。

で、Unityの実態は /opt/Unity/Editor/Unity にあるので、それを適当に -batchmode で叩けばいいんでしょって話ですが、しかし最大の関門はライセンス認証。それに関してはイメージを公開してくれているgablerouxさんのGabLeRoux/unity3d-ci-exampleや、そして日本語ではCircleCIでUnityのTest&Buildを雰囲気理解で走らせたに、手取り足取り乗っているので、基本的にはその通りに動かせば大丈夫です。

ただ、ちょっと情報が古いっぽくて、今のUnityだともう少し手順を簡単にできるので(というのを試行錯誤してたら苦戦してしまった!)、少しシンプルになったものを以下に載せます。

まず、ローカル上でライセンスファイルを作る必要があります。これはdockerイメージ上で行います。また、ここで使うイメージはCIで実際に使うイメージと同じバージョンでなければなりません。バージョン変わったらライセンス作り直しってことですね、しょーがない。そのうちここも自動化したくなるかもですが、今は手動でやりましょう。

docker run -it gableroux/unity3d:2018.3.11f1 bash
cd /opt/Unity/Editor
./Unity -quit -batchmode -nographics -logFile -createManualActivationFile
cat Unity_v2018.3.11f1.alf

イメージを落としてきて、 -quit -batchmode -nographics -logFile -createManualActivationFile でUnityを叩くと Unity_v***.alf という中身はXMLの、ライセンスファイルの元(まだuseridもpasswordも入力してないので、テンプレみたいなものです)が生成されます。こいつを、とりあえず手元(ホスト側)に持ってきます。docker cpでコンテナ->ホストにファイルを移動させてもいいんですが、まぁ1ファイルだけなのでcatしてコピペして適当に保存でもOK。

次にhttps://license.unity3d.com/manualを開いて、上記のalfファイルを上げると Unity_v2018.x.ulf ファイルがもらえます。これが実体です。生成過程でUnityのサイトにログインしているはずで、そのuserid/passwordが元になって、ライセンスファイルの実体が生成されました。中身はXMLです。

で、これは大事な情報なのでCircleCI上のEnvironment Variablesで秘匿しよう、という話になるんですが、改行の入った長いXMLなので、そのまんま中身をコピペるとファイルが、たいていどこか壊れて認証通らなくなります(散々通らないでなんでかなぁ、と悩みました!)。とはいえファイルそのものをリポジトリに上げるのはよろしくないので、CircleCIでUnityのTest&Buildを雰囲気理解で走らせたにあるとおり、暗号化したものをリポジトリに追加して、Environment VariablesにはKeyを追加しましょう。

openssl aes-256-cbc -e -in ./Unity_v2018.x.ulf -out ./Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY}

${CIPHER_KEY}は、適当な文字列に置き換えてもらって、そしてこれをCircleCI上のEnvironment Variablesにも設定します。ファイルの置き場所は、とりあえず私は .circleci/Unity_v2018.x.ulf-cipher に置きました、CIでしか使わないものなので。

またはマルチラインキーの場合は base64を使うことが推奨されているようです => Encoding Multi-Line Environment Variables。こちらのほうが良さそうですね。

あとは .circleci/config.ymlを書くだけ、ということで、最小の構成はこんな感じになります。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
workflows:
  version: 2
  build:
    jobs:
      - build-test

-nographicsにすることでそのまま叩けるのと、-manualLicenseFileでライセンスファイルを渡してやるだけです。 認証する際の || exit 0 がお洒落ポイントで、認証が正常に済んでもexit code 1が返ってくるという謎仕様なので、とりあえずこのステップは強制的に正常終了扱いにしてあげることで、なんとかなります。なんか変ですが、まぁそんなものです。世の中。

まぁしかしGabLeRoux/unity3d-ci-exampleの(無駄に)複雑な例に比べれば随分すっきりしたのではないでしょうか。いやまぁ、Unityのイメージ作ってもらってるので感謝ではあるのですけれど、しかしサンプルが複雑なのは頂けないかなあ。私はサンプルは限りなくシンプルにすべき主義者なので。

.unitypackageを作る

バッチモードでは -executeMethod により特定のstatic methodが叩けるので、それでunitypackageを作るコードを用意します。 今回は Editor/PackageExport.cs に以下のようなファイルを。

using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

// namespaceがあると動かなさそうなので、グローバル名前空間に置く
public static class PackageExport
{
    // メソッドはstaticでなければならない
    [MenuItem("Tools/Export Unitypackage")]
    public static void Export()
    {
        // configure
        var root = "Scripts/CISample";
        var exportPath = "./CISample.unitypackage";

        var path = Path.Combine(Application.dataPath, root);
        var assets = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
            .Where(x => Path.GetExtension(x) == ".cs")
            .Select(x => "Assets" + x.Replace(Application.dataPath, "").Replace(@"\", "/"))
            .ToArray();

        UnityEngine.Debug.Log("Export below files" + Environment.NewLine + string.Join(Environment.NewLine, assets));

        AssetDatabase.ExportPackage(
            assets,
            exportPath,
            ExportPackageOptions.Default);

        UnityEngine.Debug.Log("Export complete: " + Path.GetFullPath(exportPath));
    }
}

ちょっとassetsを取るところが長くなってしまっているのですが、.cs以外をフィルタするコードを入れています。たまに割と入れたくないものが混ざっていたりするので。あとは、CIではライセンス認証のあとに、これを叩くコマンドと、artifactに保存するコマンドを載せれば良いでしょう。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
workflows:
  version: 2
  build:
    jobs:
      - build-test

完璧です!

コマンドに関しては普通にWindowsのUnity.exeで試してから挑むのがいいわけですが、一つWindowsには難点があって、ログが標準出力ではなく %USERPROFILE%\AppData\Local\Unity\Editor\Editor.log にしか吐かれないということです。というわけで、Editor.logを開いてにらめっこしながらコマンドを作り込みましょう。めんどくせ。

EditorでUnitTestを行う

基本的に -runEditorTests をつけるだけなのですが、注意点としては -quit は外しましょう。ついてると正常に動きません(はまった)。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
      
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage

      - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -runEditorTests -editorTestsResultFile ./test-results/results.xml
      - store_test_results:
          path: test_results
workflows:
  version: 2
  build:
    jobs:
      - build-test

editorTestsResultFile で指定し、store_test_resultsに格納することでCircleCI上でテスト結果を見ることができます。

と、思ったんですが、なんかテスト周りは全体的にうまく動かせてないんで後でまた調べて修正します……。或いは教えてくださいです。

IL2CPP/Windowsでビルドする

なぜWindowsかというと、私がWindowsを使っているからというだけなので、その他のビルドが欲しい場合はそれぞれのビルドをしてあげると良いんじゃないかと思います!

いい加減コンフィグも長くなってきましたが、-buildWindows64Playerでビルドして、zipで固めてぽんということです。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
      
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage

      - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -runEditorTests -editorTestsResultFile ./test-results/results.xml
      - store_test_results:
          path: test_results

      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -buildWindows64Player ./bin-win64/CISample.exe
      - run: apt-get update
      - run: apt-get install zip -y
      - run: zip -r CISampleWin64Binary.zip ./bin-win64
      - store_artifacts:
          path: ./CISampleWin64Binary.zip
          destination: ./CISampleWin64Binary.zip
workflows:
  version: 2
  build:
    jobs:
      - build-test

これで一旦は希望のものは全てできました!

以上な感じが最終結果になります。

CircleCIでUnityビルドはプロダクトで使えるか

今回の例のようなライブラリ程度だと、リソースもほとんどないしリポジトリも全然小さいんでいいんですが、実プロダクトで使えるかというと、どうでしょう。まずリポジトリのサイズの問題で、次にビルド時間の問題で。クソデカい&高級マシンでも焼き上がり1時間は普通とか、そういう世界ですものね。常識的に考えてこれをクラウドでやるのは難しそう。オンプレのCircleCI Enterpriseだったら行けそうな気もしますが、どうでしょうねえ。しかしJenkinsマンやるよりは、こちらのほうが夢があるのと、実際うまくクラスタを組めば、ばかばかコンテナ立ち上げて同時並列でー、というビルドキュー長蛇列で待ちぼうけも軽減できたりで、良い未来は感じます。試してみたさはあります、あまりJenkinsに戻りたくもないし。

一回構築してみれば、ymlもそこそこシンプルだし、(ライセンス認証以外は)ymlコピペで済むので、Unity Cloud Build使わなくてもいいかなー、色々自由にできるし。っていうのはあります。というわけで、是非一緒にUnityでCircleCI道を突き進んでみましょう:) 今回はAndroidビルドやiOSビルドという面倒くさいところには一切手を付けてませんが、まぁほとんどビルドできてるわけで、やりゃあできるでしょう。いや、でもiOSとか死ぬほど面倒くさ(そう)なので、そのへんよしなにやってくれつつマシンパワーもそこそこ用意してくれるUnity Cloud Buildは偉い。

ところでこのブログ、ymlのシンタックスハイライトがない模様。やべー。このブログのメンテこそが一番最重要な気がしてきた。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive