Dockerで動かすASP.NET CoreとリバースプロキシをUnixドメインソケットでつないでみた

ASP.NET Coreの組み込みWebサーバーのKestrelはUNIXドメインソケット(UDS)で待ち受けができる、と知ったのでやってみた。

UNIXドメインソケットは同一ホストでプロセス間通信をするもの。リバースプロキシ(Webサーバー)とASP.NET Coreアプリケーションを同じホストで実行する。両者をDockerコンテナとして実行し、UNIXドメインソケットをDockerのボリューム機能で共有する。Webサーバーは触ってみたかったのでH2Oにした。

やってみた環境

> chcp 437
> systeminfo | findstr /B /C:"OS Name" /C:"OS Version"
OS Name:                   Microsoft Windows 10 Pro
OS Version:                10.0.16299 N/A Build 16299
> dotnet --version
2.1.2
> docker --version
Docker version 17.09.1-ce build 19e2cf6

Docker ImageはDocker Hubにある公式イメージを使った。(2017/12/13でのlatest)

ASP.NET Coreアプリケーション

素のASP.NET Coreアプリケーションを用意する

dotnet newコマンドの「web」テンプレートでHello Worldと返すだけのアプリケーションが作れる。アプリケーションの名前は「myapp」とする。

> mkdir myapp
> cd myapp
> dotnet new web

UNIXドメインソケットでの待ち受けを構成する

Program.csのBuildWebHostメソッドでASP.NET CoreのWebサーバー(Kestrel)がセットアップされる。WebHost.CreateDefaultBuilderの中でUseKestrel、UseIISIntegrationが呼ばれ、Kestrelの待ち受けが構成される。今回はCreateDefaultBuilderの後でさらに追加でUseKestrelを呼んでUNIXドメインソケットでのListenを追加することにした。UNIXドメインソケットのパスは「/tmp/myapp/kestrel.sock」にした。

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseKestrel(options =>
            {
                options.ListenUnixSocket("/tmp/myapp/kestrel.sock");
            })
            .UseStartup<Startup>()
            .Build();
}

公式ドキュメント: ASP.NET Core でのホスティング
WebHost.CreateDefaultBuilderのソースコード: MetaPackages/WebHost.cs at rel/2.0.0 · aspnet/MetaPackages · GitHub

ASP.NET Coreアプリケーションを実行する

dotnet publishコマンドでビルドする。docker runでアプリケーションを実行する。このときコンテナに2種類のボリュームをマウントする。一つはアプリケーションのdllを含んだホストディレクトリと対応したボリューム。もう一つはUNIXドメインソケットをコンテナ間で共有するためのホストディレクトリと対応しないボリューム。

> dotnet publish
> cd bin\Debug\netcoreapp2.0\publish
> docker run -d --rm -v /tmp/myapp -v %CD%:/app -w /app --name app microsoft/aspnetcore dotnet myapp.dll
docker run
--name app コンテナの名前をappとする
-v /tmp/myapp UNIXドメインソケットを作るディレクトリをボリュームとしてマウントする
-v %CD%:/app ビルド結果をフォルダごとDockerコンテナの/appディレクトリとしてマウントする
-w /app コンテナの作業ディレクトリを/appにする
dotnet myapp.dll コンテナ実行時のコマンドと引数

Docker run リファレンス docs.docker.jp
コンテナでデータを管理する

待ち受けを確認する

アプリケーションがUNIXドメインソケットで待ち受けしているか確認する。コンテナの中でcurlを使う。Hello Worldが返ってくればOK。

> docker exec -it app bash
# curl --unix-socket /tmp/myapp/kestrel.sock http://localhost/

curlでunix domain socket経由アクセスする
Can cURL send requests to sockets? - Super User
curl 7.50以降ではURLにホスト名を含めてよくなったらしい

リバースプロキシ

H2Oの設定ファイルを作る

yamlの設定ファイルを作る。ファイル名はh2o.confとする。接続を試したいだけなのでuser:rootとする。

hosts:
  default:
    listen:
      port: 8080
    paths:
      /:
        proxy.reverse.url: "http://[unix:/tmp/myapp/kestrel.sock]/"

access-log: /dev/stdout
error-log: /dev/stderr
user: root

公式ドキュメント: Reverse Proxy · h2o/h2o WikiProxy Directives - Configure - H2O

今回のやりかたでは、ASP.NET CoreはrootユーザーでUNIXドメインソケットを作る。H2Oもrootで実行しないとUNIXドメインソケットが読み書きできない。ちゃんと使おうと思ったらきちんとユーザーを作って設定しなければならないだろう。

H2Oを実行する

--volumes-fromで「app」コンテナのボリュームを共有する。 これで「/tmp/myapp」ディレクトリが2つのコンテナ間で共有される。 h2o.confファイルをコンテナの「/etc/h2o/」ディレクトリに置く。ファイル1つでもボリュームマウントできるが、今回はdocker cpを使ってファイルをコンテナにコピーした。

>docker create --rm --volumes-from app -p 8080:8080 --name h2o lkwg82/h2o-http2-server
>docker cp h2o.conf app:/etc/h2o/h2o.conf
>docker start h2o

できあがり

Dockerホスト1のブラウザでhttp://localhost:8080にアクセスするとアプリケーションからの応答が返ってくる。

手軽にできるようにDocker HubのイメージのDockerfileを見てdocker runのオプションを決めた。docker build のコンテキストと、DockerfileでのCOPYがこのイメージの肝。DockerfileはDockerデーモンが実行するので、docker buildしたクライアントとは見えるファイルが違う。そのためのコンテキスト。

参考にした: Dockerfileを書く時の注意とかコツとかハックとか | kim hirokuni

おまけ1

H2Oのコンテナの中でUNIXドメインソケットへアクセスできているか確認する。このイメージはalpine linuxがベースになっている。bashがないので、コマンドashで対話する。パッケージマネージャーはapkupdateしてaddでインストール。

> docker exec -it h2o ash
# apk update
# apk add curl
# curl --unix-socket /tmp/myapp/kestrel.sock http://localhost/

おまけ2

Kestrelの既定の待ち受けポートはTCP:5000。UseIISIntegration(IIS統合)が有効になるとKestrelの待ち受けポートが動的に決定され、IIS経由のリクエスト以外は拒否される。しかし、後で追加するUnixドメインソケットでの待ち受けには影響がないっぽい。

実行中のASP.NET CoreアプリケーションでWebサーバーの待ち受けがどうなっているのかIServerAddressesFeatureでわかる。

Kestrel web server implementation in ASP.NET Core | Microsoft Docs

 var serverAddressesFeature = app.ServerFeatures.Get<IServerAddressesFeature>();

片付け

> docker rm -v -f h2o app

  1. 今回はDocker for WindowsなのでWindowsからブラウザでhttp://localhost:8080でDockerで動くH2Oにアクセスできる