tech.guitarrapc.cóm

Technical updates

.NET Core App を Docker コンテナとして公開する

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 の場合環境に合わせて実行ランタイムがあるのはご存知の通りです。

www.microsoft.com

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 として公開しました。

github.com

ツールは、docker でも利用可能です。これは Dockerfile 専用のリポジトリを別途用意しています。

github.com

Docker hub はこちら。

https://hub.docker.com/r/guitarrapc/s3sync/

どんなことをしているのかを通して、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 からはこのあたりで

docs.microsoft.com

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」 してください。

https://github.com/guitarrapc/S3Sync#configuration

# 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 で実行するのが安定していて、こまったちゃんです。

github.com

まとめ

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 : [Errno 104] Connection reset by peer) でおちるのもある

*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 でも生成できるのでご随意に