Cy#の河合です。昨年12月に、﹃MagicOnion﹄というライブラリのリリースを告知しました。今回、再びオープンソースライブラリとして、C#のためのCLI/Batchライブラリをリリースしました。
[GitHub – Cysharp/MicroBatchFramework]
.NET CoreになってWindows/Mac/Linux問わずクロスプラットフォームなアプリケーション開発環境として機能するようになったC#ですが、そして機能的には十分揃っているのですが、ちょっと気の利いたフレームワークは意外と欠けているところがあります。バッチ・コマンドラインアプリ。というと地味なトピックスですが、ゆえに基本機能以上のサポートがなかったりします。しかし、﹁C#の可能性を切り開いていく﹂という理念を掲げるCy#としては、派手・地味を問わず、現状のC#に欠けているものを埋めていくことで、C#がアプリケーションを作る環境として、より良いものなるようにしたいと思っています。
これにより、dotnetコマンドが使える環境に対するCLIツールの配布、管理が容易になっています。
複雑なワークフローを定義したい場合は、別途LuigiやApache Airflowが使えるはずです。
CLIツールのためのフレームワーク
すでにC#にはコマンドライン引数の解析ツールはたくさんあります。とはいえ、そもそもそういうツールを使う時は﹁コマンドライン引数の解析﹂がしたいわけではなくて、﹁パラメータバインディング﹂をしたいのが一般的と思われます。ということで、﹃MicroBatchFramework﹄はウェブフレームワークのようにメソッドを呼び出してくれる仕様にしました。
class Program
{
static async Task Main(string[] args)
{
// エントリポイント。この1行でフレームワークのStartup設定は終わりです。
await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<MyFirstBatch>(args);
}
}
// 実際のバッチの定義
public class MyFirstBatch : BatchBase // BatchBaseを継承してもらって
{
// void(同期)かTask(非同期)の戻り値が設定可能、パラメータはどんな型も自由(JSONからマッピングします)
public void Hello(string name, int repeat = 3)
{
for (int i = 0; i < repeat; i++)
{
this.Context.Logger.LogInformation($"Hello My Batch from {name}");
}
}
}
これは `SampleApp.exe -name “foo” -repeat 5` で呼び出すことが可能、という仕様です。
// 引数に別名や説明をつけたり public void Hello( [Option("n", "name of send user.")]string name, [Option("r", "repeat count.")]int repeat = 3) { // ...omit } // サブコマンドの作成や、引数順番でもバインディングなどをサポート [Command("escape")] public void UrlEscape([Option(0)]string input) { Console.WriteLine(Uri.EscapeDataString(input)); }煩わしい定型的な引数の解析に悩まされることなく、メソッドだけ定義すれば、あとはすべてよしなにやってくれます。 .NET Coreで作成したアプリは、ランタイムのインストールも不要な実行ファイルを Windows/Linux/Mac向けに作ることができます。また、CIに関してもCircleCIのようなコンテナベースのCIを、他の言語と遜色なく利用することが可能です。例えば以下のような記述で、実行ファイル生成をCIに任せられます。
version: 2.1 executors: dotnet: docker: - image: mcr.microsoft.com/dotnet/core/sdk:2.2 environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true NUGET_XMLDOC_MODE: skip jobs: publish-all: executor: dotnet steps: - checkout - run: dotnet publish -c Release --self-contained -r win-x64 -o ./MicroBatchSample/win-x64 - run: dotnet publish -c Release --self-contained -r linux-x64 -o ./MicroBatchSample/linux-x64 - run: dotnet publish -c Release --self-contained -r osx-x64 -o ./MicroBatchSample/osx-x64 - run: apt update && apt install zip -y - run: zip -r MicroBatchSample.zip ./MicroBatchSample - store_artifacts: path: MicroBatchSample.zip destination: MicroBatchSample.zip workflows: version: 2 publish: jobs: - publish-allこの辺りは、もはやC#だからといってWindowsに縛られたり、使えるシステムに制約があったりということはなく、自由に組み合わせていくことができる証左と言えるでしょう。 また、配布に関しては.NET CoreグローバルツールというNPM Global Toolsのような仕組みも用意されています。例えば以下のようなUlid生成のためのツールをMicroBatchFrameworkと.NET Coreグローバルツールで作ってみました。
![55695191-6603e600-59f2-11e9-8553-72d03937fd57](https://tech.cygames.co.jp/wp-content/uploads/2019/04/55695191-6603e600-59f2-11e9-8553-72d03937fd57.png)
バッチのためのフレームワーク
C#とバッチはけっこう相性が良いと思っています。その最たるところは並列処理で、とりあえず実行時間かかりそうなものはParallelにするだけで、バッチ突き抜け率を大幅に低減することができます。// これを foreach(var item in foo) { // なんかたくさん処理する } // こうするだけ Parallel.ForEach(item => { // なんか色々やる });というのはともかく、実際にプロダクトで使うバッチ処理は複数、むしろ山のように用意することがほとんどだと思います。それらを単一のコンソールアプリケーションで効率よく管理できるように、クラスとメソッドを定義するだけで出し分けられるシステムを用意しました。
class Program { static async Task Main(string[] args) { // <T>を渡さないと複数検索モードになる await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync(args); } } // こんなクラスや public class Foo : BatchBase { // こんなメソッドや public void Echo(string msg) { this.Context.Logger.LogInformation(msg); } // こんなメソッドを public void Sum(int x, int y) { this.Context.Logger.LogInformation($"{x + y}"); } } // こんなクラスを public class Bar : BatchBase { public void Hello2() { this.Context.Logger.LogInformation("H E L L O"); } }このように、第1引数によって実行するメソッドを切り替えられます。
SampleApp.exe Foo.Echo -msg "aaaaa" SampleApp.exe Foo.Sum -x 100 -y 200 SampleApp.exe Bar.Hello2さて、こうして出来たアプリケーションは、コンテナ化するのをお薦めします。.NET CoreならばC#でもコンテナ化は簡単です。
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS sdk WORKDIR /workspace COPY . . RUN dotnet publish ./MicroBatchFrameworkSample.csproj -c Release -o /app FROM mcr.microsoft.com/dotnet/core/runtime:2.2 COPY --from=sdk /app . ENTRYPOINT ["dotnet", "MicroBatchFrameworkSample.dll"]例えばこれをAWS ECRにCircleCIで上げるなら、以下のようなコンフィグになるでしょう。
version: 2.1 orbs: aws-ecr: circleci/aws-ecr@3.1.0 workflows: build-push: jobs: # see: https://circleci.com/orbs/registry/orb/circleci/aws-ecr - aws-ecr/build_and_push_image: repo: "microbatchsample"非常にシンプルに書けるし、C#だからといって特別なことはなく、一般的なエコシステムに乗っているわけです。 こうして上がったコンテナは、あとはただのコンテナなので好きなように実行すればいいわけですが、例えばAWS Batchを使えば比較的シンプルに実行もスケジューリング︵CloudWatchでイベントトリガを設定可能︶も定義可能です。そして、ログはCloudWatchで標準出力が確認できます。
![55616375-a6821a80-57cc-11e9-9d3a-a0691e631f28](https://tech.cygames.co.jp/wp-content/uploads/2019/04/55616375-a6821a80-57cc-11e9-9d3a-a0691e631f28.png)
Swaggerによるウェブインターフェイス
開発時など、いちいちコマンド打って確認するのも面倒な場合もあるため、ウェブでホストする機能を付けました。スタートアップとして`RunBatchEngineAsync`の代わりに`RunBatchEngineWebHosting`を使うだけです。public class Program { public static async Task Main(string[] args) { await new WebHostBuilder().RunBatchEngineWebHosting("https://localhost:12345"); } }これはSwaggerによってホストされるため、実行確認等が容易になります。
![55614839-e8a95d00-57c8-11e9-89d5-ab0e7830e401](https://tech.cygames.co.jp/wp-content/uploads/2019/04/55614839-e8a95d00-57c8-11e9-89d5-ab0e7830e401.png)
MicroBatchFrameworkの.NET的な新しさ
.NET Generic Hostの上にコンソールアプリケーションを構築するというのが、MicroBatchFrameworkの新しさです。 .NET Generic Hostは、標準的な仕組みとしてロギング/コンフィグ読み込み/DIをサポートしています。これによりコンフィグのマッピング、ロギングなどを標準的な作法でフルサポートしています。// appconfig.json { "Foo": 42, "Bar": true } class Program { static async Task Main(string[] args) { await BatchHost.CreateDefaultBuilder() .ConfigureServices((hostContext, services) => { // mapping config json to IOption<MyConfig> services.Configure<MyConfig>(hostContext.Configuration); }) .RunBatchEngineAsync<MyFirstBatch>(args); } } public class MyFirstBatch : BatchBase { IOptions<MyConfig> config; ILogger<MyFirstBatch> logger; // get configuration from DI. public MyFirstBatch(IOptions<MyConfig> config, ILogger<MyFirstBatch> logger) { this.config = config; this.logger = logger; } public void ShowOption() { logger.LogInformation(config.Value.Bar); logger.LogInformation(config.Value.Foo); } }これをやっているフレームワークって意外とないのです︵なぜなら.NET Generic Host自体が最近出来た仕組みなので︶。この辺りもまた﹁現状のC#に欠けているものを埋めていく﹂ということです。