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と同期するためにツールを作りました。*10数年前に 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 触るだけのためにその構成は苦しい。ということで、ベースとしつつ組んでいます。*11
Microsoft からはこのあたりで
Docker からも出てるのでこのあたりみつつがいいです。
https://docs.docker.com/engine/examples/dotnetcore/docs.docker.com
Docker for Windows / Docker for Mac だけ入れておくと Docker 操作ができます。*12
さて、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 を利用します。*13
同期パラメーターは、引数か、環境変数で指定できます。デフォルトで 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:aws clie の s3 sync だと大量データの同期がCaused by
*11:dotnet restore できなくするのはワカルけどその解決でなぜ納得すると思うのか
*12: Linux Containers on Windows (LCOW)を使う - http://www.misuzilla.org/Blog/2017/12/27/Lcow を使うともうちょい楽そうです
*13:aws configure でも AWS Tools for PowerShell でも VS でも生成できるのでご随意に