C# を使っていて最も困るのがランタイムと感じます。
C#は書きやすい、Visual Studioも使いやすいは良く耳にします。実際享受しやすいメリットですが、C# をWindows以外で実行したいはどうでしょうか?
今回はその実行方法についてコンテナを用いるのはどうかと考えたメモです。
※ モバイルではなく、サーバー/デスクトップにおけるアプリケーションについて考えます。
課題
.NETを実行するときにはランタイムが必要です。現在、 Windows / macOS / Linuxなど色々な環境で同じように動かす場合は .NET Coreが用いられます。簡単に実行確認したい、さくっと試したい、使いたいというユースケースが大半を占めるのではないでしょうか。
これをはじめに考えてみます。
ランタイムの導入
ローカル開発でランタイムが入っている方が便利と感じるシーンは多いです。これは、エディタ/IDEでの開発でSDKやランタイム参照をしつつ動的に解決することが多いためで、「開発したい」に対して「インストール、導入」という手間なコストをかけても割が合います。またツーリングからみても、今はまだランタイムをインストールする方が様々なツールとの統合も楽でしょう。*1
ではサクッと動かしてアプリを試したい。場合はどうでしょうか? 例えば、 .NET Coreで書いたC# アプリをmacOSで動かしたい、Linuxで動かしたい*2、あるいはRasPiで動かしたい、むしろWindows Serverでは? このような色々なプラットフォームで動かす場合は事情が変わるるでしょう。
私は、動かしたいプラットフォームに合わせて「動かす」、ただこれだけのためにランタイムを入れるのは大変と感じるぐらいにはぐうたらです。いい感じのアプリがあった! ワンオフで動かしたい! .NET Coreを使って動かせばいいの?そのランタイムはどうやって入れるの?なるほど、ランタイムをダウンロードしてインストールをこのアプリだけのためにするの.... 更新は? 実行の保証は? 面倒です。*3
例えば .NET Coreの場合環境に合わせて実行ランタイムがあるのはご存知の通りです。



C# をWindowsで実行する分には.NET Frameworkが初めから入っていることもあり気にならなかったことが、マルチプラットフォームだとおっくうに感じるのが、今*4のC#に感じる障壁の高さと思っています。
実行保証
「アプリのロジック」ではなく、「アプリをどう実行するか」がC# にとっては大きな課題に感じると書きました。*5
ほかにも「プラットフォームでの実行保証」が気になります。環境によって動かしてみたら予想外の挙動をした。というのは、C# に限らず良くあることで様々な言語が平等に持つ課題です。
これも解決は単純で、手元に環境があればいいでしょう。では、環境どうやりましょうか、めんどくさくないですか?
コンテナはどうなのか
これらの課題は、Dockerをはじめとしたコンテナ*6で一定の改善が期待できます。*7
例えば配布するコンテナイメージにランタイムが入っていれば、利用する側はdocker runするだけでランタイムを隠蔽*8して実行できます。これは多くの言語で作成されたツールが活用しているようにC# だってもちろん同様です。
また、Windows・macOS両方でローカルコンテナ動作できれば、そのコンテナイメージを配布することで同じ挙動が期待できます。ポータビリティが重要なのは言わずもがなです。
S3Sync - .NETCore で S3同期するツール
x00万を超える大量のアセットファイルを配信したいということをしたくなったときに、S3などのオブジェクトストレージがパット思いつきます。*9これを現実的な速度でS3と同期するためにツールを作りました。((aws clieのs3 syncだと大量データの同期がCaused by <class 'socket.error'>:[Errno 104]Connection reset by peer)でおちるのもある))数年前にPowerShellで同様の同期ツールを書いたのですがx000ファイルを対象に書いていて大量のファイルだと遅かったのでC# で書き直ししたものです。
GitHubにてS3Syncとして公開しました。
ツールは、dockerでも利用可能です。これはDockerfile専用のリポジトリを別途用意しています。
Docker hubはこちら。
どんなことをしているのかを通して、Dockerとして公開する良さを考えてみます。
Docker hub での公開用リポジトリ
もともと単一リポジトリにしていたのですが、Docker hubでのautomated buildとコードの更新/バージョン管理とずれるため分離しました。他のDocker Automated buildをしている人も同様にしているようです。これが素直なのかなと考えますがいいアイデアあったら教えていただきたく...。
今は、s3sync-dockerリポジトリのmater/tagを使って自動ビルドしています。

これでバージョンに応じたタグでイメージも公開されるので楽ちんです。

docker hub用のDockerfileは、コード側リポジトリの指定バージョンのリリースに仕込んだバイナリを持ってくるようにして、コードのバージョンとコンテナのバージョンを合わせています。
FROM microsoft/dotnet:2.0-runtime
WORKDIR /app
ENV S3Sync_LocalRoot=/app/sync
RUN curl -sLJO https://github.com/guitarrapc/S3Sync/releases/download/1.2.0/s3sync_netcore.tar.gz \
&& tar xvfz s3sync_netcore.tar.gz \
&& rm ./s3sync_netcore.tar.gz
CMD ["dotnet", "S3Sync.dll"]

.NET Core と .NET4.7 ビルド対応
両方のビルド対応は、凝ったことはしておらず<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>のみです。
https://github.com/guitarrapc/S3Sync/blob/master/source/S3Sync/S3Sync.csproj#L6
Docker でのアセンブリビルド
Visual StudioのDocker Supportが、いまいちイケテナイというか結構癖あってdocker触るだけのためにその構成は苦しい。ということで、ベースとしつつ組んでいます。*10
Microsoftからはこのあたりで
https://docs.microsoft.com/ja-jp/dotnet/core/docker/building-net-docker-images
Dockerからも出てるのでこのあたりみつつがいいです。
Docker for Windows / Docker for Macだけ入れておくとDocker操作ができます。*11
さて、dotnet buildビルドを、VSとdockerのどちらでも行えます。Dockerを使ったビルドが楽なのは手元にdotnetランタイムがなくてもビルドできることで、ビルド結果がどのOSでも変わらず取得できます。.NETCoreをビルドするのにランタイムが必要、という都合を気にしないで使うというのは良さを感じます。
Dockerでのビルドをするにあたり、以下のようなdocker-compose.ymlを用意してあります。
version: '3'
services:
ci-build:
image: microsoft/dotnet:2.0-sdk
volumes:
- .:/src
working_dir: /src
command: /bin/bash -c "dotnet restore ./S3Sync.sln && dotnet publish ./S3Sync/S3Sync.csproj -c Release -o ./obj/Docker/publish -f netcoreapp2.0"
S3Sync/sourceパスでdocker-compose -f docker-compose.ci.build.yml upをコマンド実行するとdockerコンテナ内部でdotnet buildが実行され、S3Sync/source/S3Sync/obj/docker/publishにdotnet buildによって生成されたアーティファクトができます。
$ docker-compose -f docker-compose.ci.build.yml up
Starting source_ci-build_1 ... Starting source_ci-build_1 ... done Attaching to source_ci-build_1 ci-build_1 | Restoring packages for /src/S3Sync.BenchmarkCore/S3Sync.BenchmarkCore.csproj... ci-build_1 | Generating MSBuild file /src/S3Sync.BenchmarkCore/obj/S3Sync.BenchmarkCore.csproj.nuget.g.props. ci-build_1 | Generating MSBuild file /src/S3Sync.BenchmarkCore/obj/S3Sync.BenchmarkCore.csproj.nuget.g.targets. ci-build_1 | Restore completed in 225.41 ms for /src/S3Sync.BenchmarkCore/S3Sync.BenchmarkCore.csproj. ci-build_1 | Restoring packages for /src/S3Sync.Core/S3Sync.Core.csproj... ci-build_1 | Restoring packages for /src/S3Sync/S3Sync.csproj... ci-build_1 | Generating MSBuild file /src/S3Sync/obj/S3Sync.csproj.nuget.g.props. ci-build_1 | Generating MSBuild file /src/S3Sync.Core/obj/S3Sync.Core.csproj.nuget.g.props. ci-build_1 | Generating MSBuild file /src/S3Sync/obj/S3Sync.csproj.nuget.g.targets. ci-build_1 | Generating MSBuild file /src/S3Sync.Core/obj/S3Sync.Core.csproj.nuget.g.targets. ci-build_1 | Restore completed in 78.35 ms for /src/S3Sync.Core/S3Sync.Core.csproj. ci-build_1 | Restore completed in 74.78 ms for /src/S3Sync/S3Sync.csproj. ci-build_1 | Microsoft (R) Build Engine version 15.5.179.9764 for .NET Core ci-build_1 | Copyright (C) Microsoft Corporation. All rights reserved. ci-build_1 | ci-build_1 | Restore completed in 18.76 ms for /src/S3Sync.Core/S3Sync.Core.csproj. ci-build_1 | Restore completed in 3.33 ms for /src/S3Sync/S3Sync.csproj. ci-build_1 | S3Sync.Core -> /src/S3Sync.Core/bin/Release/netcoreapp2.0/S3Sync.Core.dll ci-build_1 | S3Sync -> /src/S3Sync/bin/Release/netcoreapp2.0/S3Sync.dll ci-build_1 | S3Sync -> /src/S3Sync/obj/Docker/publish/ source_ci-build_1 exited with code 0

$ ls S3Sync/obj/Docker/publish

Docker コンテナビルド
コンテナと配布するにはローカル実行を試しておきたいので、コンテナイメージのビルドもしましょう。これも以下のようなdocker-compose.ymlを用意してあります。
version: '3'
services:
s3sync:
image: guitarrapc/s3sync
build:
context: ./S3Sync
dockerfile: Dockerfile
ローカルビルド用のDockerfileは次のようなものです。
FROM microsoft/dotnet:2.0-runtime ARG source WORKDIR /app ENV S3Sync_LocalRoot=/app/sync COPY ${source:-obj/Docker/publish} . CMD ["dotnet", "S3Sync.dll"]
S3Sync/sourceパスでdocker-compose buildをコマンド実行することでdockerイメージがビルドできます。
$ docker-compose build
Building s3sync
Step 1/6 : FROM microsoft/dotnet:2.0-runtime
---> c3e88dec1c1a
Step 2/6 : ARG source
---> Using cache
---> 647c269a901b
Step 3/6 : WORKDIR /app
---> Using cache
---> 6b3ed8b5ba59
Step 4/6 : ENV S3Sync_LocalRoot /app/sync
---> Using cache
---> 0e5b9c7353eb
Step 5/6 : COPY ${source:-obj/Docker/publish} .
---> 9eef14226f86
Step 6/6 : CMD dotnet S3Sync.dll
---> Running in 8ceb9cd9194d
---> 7e32766ae910
Removing intermediate container 8ceb9cd9194d
Successfully built 7e32766ae910
Successfully tagged guitarrapc/s3sync:latest

イメージの生成はdocker image lsで。
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE guitarrapc/s3sync latest b33b90c72cee 6 hours ago 220MB
ローカル実行
exe / .NETCore / Dockerのいずれもローカル実行ができます。IAM Roleで認証をバイパスできない場合は、AWS Credential Profileを利用します。*12
同期パラメーターは、引数か、環境変数で指定できます。デフォルトでDryRunが有効になっているので、同期を実行する場合は「引数でDryRun=false」か「環境変数でS3Sync_DryRun=false」してください。
# Full .NET $ S3Sync.exe BucketName=your-fantastic-bucket LocalRoot=C:/Users/User/HomeMoge DryRun=false
# dotnet core $ dotnet S3Sync.dll BucketName=your-awesome-bucket LocalRoot=/Home/User/HogeMoge DryRun=false
# docker $ docker run --rm -v <YOUR_SYNC_DIR>:/app/sync/ -e S3Sync_BucketName=<YOUR_BUCKET_NAME> -e AWS_ACCESS_KEY_ID=<YOUR_ACCESS_KEY> -e AWS_SECRET_ACCESS_KEY=<YOUR_SECRET> S3Sync_DryRun=false guitarrapc/s3sync
DockerイメージはWindows/mobyLinux/macOS上で動作を確認しています。
速度
ベンチマークを測る中で速度自体は、.NET4.7も .NETCore2.0もあまりずれは出ていません。

.NET Coreで遅くなるかもと思っていたので、なるほど計測大事。
ec2で実行しているのですが、CIの記録では20000ファイルで20sec程度のようです。350000ファイルぐらいだと、初回のアップロードが6minで、以降差分であれば100sec程度のようです。
Complete : Calculate Diff. 10.01sec ----------------------------------------------- Start : Upload to S3. New = 0, Update = 0) ----------------------------------------------- Complete : Upload to S3. 0.11sec ----------------------------------------------- Start : Delete on S3. (0 items) ----------------------------------------------- Complete : Delete on S3. 0sec =============================================== Detail Execution Time : ----------------------------------------------- Obtain S3 Items : 3.32sec Calculate Diff : 10.01sec Upload to S3 : 0.11sec Delete on S3 : 0sec ----------------------------------------------- Total Execution : 13.44sec, (0.22min) =============================================== =============================================== Show Synchronization result as follows. =============================================== | TotalCount | New | Update | Skip | Remove | | ----------: | ---: | ------: | -----: | ------: | | 20000 | 0 | 0 | 20000 | 0 | Complete : Synchronization. 13.69sec Total. 20.54sec
MSDeployのようなファイル同期だと恐ろしく時間がかかりますが、S3などオブジェクトストレージだと手早くできるのはいいですね。
課題
.NETCoreだと、大量のファイルを送信すると時々通信が打ち切られる現象を確認しています。何度か遭遇しているのですが、発生原因がいまいち見えず困っています。そのため、.NET4.7で実行するのが安定していて、こまったちゃんです。
まとめ
docker runでC# で書いたアプリが実行できる。dockerを日常的に触っていると、アプリを試したりどこか環境を変えて利用するには一番楽です。
利用する側にとって「使うための準備を最小限にする」というのは重要だと考えています。この意味で、これから .NET Coreで書かれたアプリがどんどんDockerで公開されるといいですね。特に、ASP.NET Core MVCとかはnginx同様ホスティングするだけなのでやりやすいわけで。
*1:APIやHTTP(S) など通信で隠蔽できない場合を想定しています
*2:ディストリは本質ではないのでおいておきましょう
*3:もちろんやってきたのですが、面倒だと思っています
*4:そしてこれから
*5:個人の感想です。私がC# を各環境で動かすにあたっていつも感じる感想であって、読んでいる方にとっては別の課題をお持ちでは?
*6:ハイパバイザーでいい人はそれでいいんじゃないでしょうか
*7:必ずしも最善ではないですが、現状では現実解として妥当でしょう
*8:カプセル化と言い換えてもいいです
*9:Blob・GCSでも好みで
*10:dotnet restoreできなくするのはワカルけどその解決では納得できない
*11: Linux Containers on Windows (LCOW)を使う - http://www.misuzilla.org/Blog/2017/12/27/Lcowを使うともうちょい楽そうです
*12:aws configureやAWS Tools for PowerShell、VSでも生成できるのでご随意に