LoginSignup
40

More than 3 years have passed since last update.

Docker Desktop の復習と、Windows Container に入門: Docker Desktop + Linux Container 復習編

Last updated at Posted at 2019-03-18

以前、Windows Native な Docker Container を試した際、Image が 10 GB 近くあったため、そっ閉じしたままになっていた。
それが、風の噂で色々進んでいるよと聞いたので、もう一度しっかり入門してみる。

Docker Desktop の復習と、Windows Container に入門: Docker Desktop + Linux Container 復習編
Docker Desktop の復習と、Windows Container に入門: Windows Server Container 理論編
Docker Desktop の復習と、Windows Container に入門: Windows Hyper-V Container, LCOW 理論編
Docker Desktop の復習と、Windows Container に入門: 実践編

まずは、Windows と Docker との歴史をまとめながら、使い慣れた Docker Desktop + Linux Container について、復習していく。

Docker

Docker については、既に素晴らしい入門が他に存在しているので割愛する。
全容をしっかり知りたいのであれば、英語だが公式を見ると良いと思う。
https://docs.docker.com/get-started/

日本語であれば、以下が最も網羅的な解説となっている。
https://employment.en-japan.com/engineerhub/entry/2019/02/05/103000

Container と Windows

1. 背景

上記紹介記事にもあるが、元々 Docker は Linux の持つ cgroup, Namespace, chroot 等の機能を利用して構築 されており、他の Platform へ簡単に移植することはできなかった。

その為、Windows や Mac OS では、VirtualBox や xhyve, Hyper-V 上に Linux VM を構築し、それを Host Machine からできるだけ透過的に操作できるように工夫していた。

しかし、Microsoft は早い段階から Windows Native な Container の実現に前向だった。

2. 沿革

● 2013/3 - Docker を OSS 化
この頃 Windows ユーザは、VirtualBox 等に VM を立てて、その中で Docker を利用していた。

● 2014/4 - Boot2Docker v0.2 がリリース
これにより、VM, Guest OS, Docker, MSYS base Terminal がワンパッケージで導入され、アイコンワンクリックで Docker が使えている に見えるようになった。
とはいえ、 Volume や Network の統合は無く、結局現実に呼び戻される。

● 2014/10 - Microsoft と Docker が協業を発表
Windows Server への Docker Engine 統合、Windows Native Client 開発、Dockerhub による Windows Container Image 管理の実現を発表した。

● 2014/11 - Docker CLI for Windows がリリース
ここで初めて Windows Native で動く Docker Client が生まれた。
しかし、相変わらず Docker が動いているのは VM 上の Linux だ。

● 2015/5 - Windows Server 2016 Technical Preview 2 リリース
Windows Nano Server が提供される。

● 2015/8 - Windows Server 2016 Technical Preview 3 リリース
念願の Windows Server Container が提供される。

● 2015/11 - Windows Server 2016 Technical Preview 4 リリース
少し遅れて Hyper-V Container が提供される。

● 2016/4 - Windows Server 2016 Technical Preview 5 リリース
Windows Container Image の DockerHub での利用が可能に。
ただし、この時点での WindowsServerCore Image はディスク上で 約 9 GB, WindowsNanoServer Image でも 約 600 MB と、Linux Container 並の Portability を実現するには少し辛いサイズであった。

● 2016/7 - Docker for Mac/Windows が正式リリース
OS Native Hypervisor ( Win: Hyper-V, Mac: xhyve ) を利用した Docker アプリケーション。
Docker が動くのが VM 上の Linux であることに変わりは無いが、Volume や Network 周りが見事に統合されていて、ホストマシン上で直接操作しているかのような使用感が得られる。

● 2016/8 - Windows 10 Pro が Hyper-V Container に対応
Desktop OS でも Windows Container が利用できるようになった。

● 2017/9~10 - Windows 10 Fall Creators Update と Windows Server 1709 で LCOW ( Linux Containers on Windows ) に対応
Windows 版 Docker Engine での Linux Container 立ち上げが可能に。

● 2018/8 - Windows Container Image のサイズがどんどん小さくなっていく
この時点での WindowsServerCore Image はディスク上で 約 3.6 GB, WindowsNanoServer Image でも 約 100 MB 未満

● 2018/8 - Docker for Windows/Mac の 2.0.0.0 がリリース。同時に名称を Docker Desktop for Windows/Mac に変更

● 2019/2 - Docker Desktop 2.0.0.2 で Windows 10 Pro が Windows Server Container に対応

いよいよ環境が全て整った。

3. 用語の整理

:book: Linux Container

Linux Kernel で動作する Container のこと。

:book: Windows Container

Windows の NT Kernel で動作する Container のこと。
場合によって呼び方は異なるが、多分公式にもこう呼ばれているはず。

:book: Windows Server Container

Windows Container の実現方法の 1 つ。
Process レベルで分離される。

Windows process container とも呼ばれる。

:book: Hyper-V Container

Windows Container の実現方法の 1 つ。
kernel レベルで分離される。

Windows Hyper-V container とも呼ばれる。

:book: LCOW ( Linux Containers on Windows )

Windows Native Docker Engine によって Linux Container が動かせる機能。
技術的には Hyper-V Container とほぼ同じで、Hyper-V 上で小さな Linux VM を立ち上げて、そこで実行される。

Docker Desktop エコシステム復習

以降では、Docker Desktop + Linux Container エコシステムについて復習していく。

従来の構成は、Hyper-V 上に設けられた完全な VM 上にある Docker Daemon に、Windows 上の Docker Client で接続して操作する。
↓ ざっくりとしたイメージ図

以下、重要な部分だけ確認していく。

Windows

まずは、Windows 側がどうなっているかを見ていく。
起動している関連サービスは、以下。

PS> ps | wsl grep -i -e ProcessName -e '---' -e  docker -e vpnkit
# Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
# -------  ------    -----      -----     ------     --  -- -----------
#     577      11    10408      18672      11.89  27256  13 com.docker.proxy
#    4385     111   184944      50004             20764   0 com.docker.service
#    1042      64   119364      91780      17.83  23480  13 Docker Desktop
#      35       3      484       2052              8376   0 Docker.Watchguard
#      35       4      512       2068              8992   0 Docker.Watchguard
#     255      16    20516      29680              7912   0 dockerd
#     641      66    20412      12184      15.80  28084  13 vpnkit

dockerd, vpnkit 以外は多分ソースが公開されていないと思われる。
その為、本記事の Docker Desktop, com.docker.service, com.docker.proxy, Docker.Watchguard に関する解説は、全て外面的な情報を元にした推測であるということ、くれぐれも注意されたし

● Docker Desktop
Docker エコシステム全体を統括するプロセス。
各サービスの初期化や起動/再起動/停止、設定変更やアップデートを行う。

● dockerd
Linux Container Mode では dockerd, Container 含め全て LinuxKit 上にあり、Windows 側の dockerd は何もしていないと思われる。

● com.docker.service
Docker 関連サービスの親サービス。
com.docker.proxy, vpnkit, Docker.Watchguard 等を子サービスとして持つ。
このサービス自体が何をしているかは不明。

● com.docker.proxy
Docker Daemon API を LinuxKit 上へと Proxy するサービス。
詳細後述。

● vpnkit
LinuxKit からの Outbound Packet の Host への転送や、Port Forwarding Packet の転送を行うサービス。
詳細後述。

● Docker.Watchguard
全くの謎。

Linuxkit

Container 用 OS をビルドするためのツールキット、またはそれによりビルドされた OS のこと。
https://github.com/linuxkit/linuxkit

YAML 定義を元に Image がビルドされる。
Desktop Docker の場合は、インストール時 ( アップグレード時も? ) に最新 Image を取得して、Hyper-V 上に展開してくれる。

接続

どんな Image なのか調査する為、Linuxkit に繋ぎたかったのだが、sshd が見つからなくて、Hyper-V Manager からの接続もできないので、裏技 を使って中に入る。

$ uname -a
# Linux docker-desktop 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 Linux

### ビルド時に利用された定義は、以下にコピーされている
$ cat /etc/linuxkit.yml
# kernel:
#   image: linuxkit/kernel:4.9.125-4ffac525e6a57ccc3f2a8ae0fb96f12169027759-amd64
#   cmdline: console=ttyS0 page_poison=1 vsyscall=emulate panic=1
# ...

Build

LinuxKit のビルドは、linuxkit.ymlkernelinitonbootonshutdownservicesfiles セクションの順に実行される。全ての処理が、Container Image の展開か Container の実行で行われる。

kernel セクションで kernel を /boot フォルダに展開し、init セクションで、Containerd, RunC, getty 等が導入されている。

$ ctr version
# Client:
#   Version:  v1.1.2
#   Revision: 468a545b9edcd5932818eb9de8e72413e616e86e
# 
# Server:
#   Version:  v1.1.2
#   Revision: 468a545b9edcd5932818eb9de8e72413e616e86e
$ runc -v
# runc version 1.0.0-rc5+dev
# commit: 69663f0bd4b60df09991c08812a60108003fa340
# spec: 1.0.0

onboot セクションにある定義は、直接 runc を呼び出して実行される。
各種初期設定を行った残骸が残っている。

$ runc list
# ID                         PID         STATUS      BUNDLE                                        CREATED                        OWNER
# 000-metadata               0           stopped     /containers/onboot/000-metadata               2019-02-20T21:17:29.2710123Z   root
# 001-sysfs                  0           stopped     /containers/onboot/001-sysfs                  2019-02-20T21:17:30.6890069Z   root
# 002-binfmt                 0           stopped     /containers/onboot/002-binfmt                 2019-02-20T21:17:31.6772885Z   root
# 003-sysctl                 0           stopped     /containers/onboot/003-sysctl                 2019-02-20T21:17:32.0187455Z   root
# 004-format                 0           stopped     /containers/onboot/004-format                 2019-02-20T21:17:32.5950764Z   root
# 005-extend                 0           stopped     /containers/onboot/005-extend                 2019-02-20T21:17:33.834064Z    root
# 006-mount                  0           stopped     /containers/onboot/006-mount                  2019-02-20T21:17:41.5659989Z   root
# 007-swap                   0           stopped     /containers/onboot/007-swap                   2019-02-20T21:17:43.3930679Z   root
# 008-move-logs              0           stopped     /containers/onboot/008-move-logs              2019-02-20T21:17:50.9157579Z   root
# 009-mount-docker           0           stopped     /containers/onboot/009-mount-docker           2019-02-20T21:17:51.5884119Z   root
# 010-mount-kube-images      0           stopped     /containers/onboot/010-mount-kube-images      2019-02-20T21:17:52.2584598Z   root
# 011-bridge                 0           stopped     /containers/onboot/011-bridge                 2019-02-20T21:17:52.5884599Z   root
# 012-vpnkit-9pmount-vsock   0           stopped     /containers/onboot/012-vpnkit-9pmount-vsock   2019-02-20T21:17:52.9334929Z   root
# 013-rngd1                  0           stopped     /containers/onboot/013-rngd1                  2019-02-20T21:17:53.5926046Z   root
# 014-windowsnet             0           stopped     /containers/onboot/014-windowsnet             2019-02-20T21:17:53.963468Z    root

Linuxkit は、基本的に読み込み専用なので、全てのサービスを Container として立ち上げている。
services セクションにある定義は、containerd により services.linuxkit Namespace で実行される。

$ ctr namespace ls
# NAME              LABELS
# services.linuxkit

$ ctr -n services.linuxkit container ls
# CONTAINER                IMAGE    RUNTIME
# acpid                    -        io.containerd.runtime.v1.linux
# diagnose                 -        io.containerd.runtime.v1.linux
# docker                   -        io.containerd.runtime.v1.linux
# kmsg                     -        io.containerd.runtime.v1.linux
# rngd                     -        io.containerd.runtime.v1.linux
# socks                    -        io.containerd.runtime.v1.linux
# trim-after-delete        -        io.containerd.runtime.v1.linux
# vpnkit-forwarder         -        io.containerd.runtime.v1.linux
# vpnkit-tap-vsockd        -        io.containerd.runtime.v1.linux
# vsudd                    -        io.containerd.runtime.v1.linux
# write-and-rotate-logs    -        io.containerd.runtime.v1.linux

最終的には、こんな Process Tree となる。

$ pstree
# init-+-containerd-+-containerd-shim---acpid
#      |            |-containerd-shim---diagnosticsd
#      |            |-containerd-shim-+-docker-init---entrypoint.sh-+-logwrite---kubelet
#      |            |                 |                             |-logwrite---lifecycle-serve---transfused.sh
#      |            |                 |                             `-start-docker.sh---dockerd-+-containerd-+-7*[containerd-shim---pause]
#      |            |                 |                                                         |            |-containerd-shim---etcd
#      |            |                 |                                                         |            |-containerd-shim---kube-apiserver
#      |            |                 |                                                         |            |-containerd-shim---kube-controller
#      |            |                 |                                                         |            |-containerd-shim---kube-scheduler
#      |            |                 |                                                         |            |-containerd-shim---kube-proxy
#      |            |                 |                                                         |            |-2*[containerd-shim---coredns]
#      |            |                 |                                                         |            |-containerd-shim---nsenter---sh---pstree
#      |            |                 |                                                         |            `-containerd-shim---nginx---nginx
#      |            |                 |                                                         `-vpnkit-expose-p
#      |            |                 |-rpc.statd
#      |            |                 `-rpcbind
#      |            |-containerd-shim---kmsg
#      |            |-containerd-shim---rngd
#      |            |-containerd-shim
#      |            |-containerd-shim---trim-after-dele
#      |            |-containerd-shim---vpnkit-forwarde
#      |            |-containerd-shim---vpnkit-tap-vsoc---vpnkit-tap-vsoc
#      |            |-containerd-shim---vsudd
#      |            `-containerd-shim---logwrite
#      |-memlogd
#      `-rungetty.sh---login---sh

dockerd

肝心の dockerd は、services セクションで起動された docker-init Container 上で起動されている。
自身から fork した形で Container Process をぶら下げているので、docker.sock を mount しない方の dind っぽくなっている。

$ ctr --namespace services.linuxkit tasks exec --exec-id 1000 docker docker version
# Client: Docker Engine - Community
#  Version:           18.09.2
#  API version:       1.39
#  Go version:        go1.10.8
#  Git commit:        6247962
#  Built:             Sun Feb 10 00:11:44 2019
#  OS/Arch:           linux/amd64
#  Experimental:      false
# 
# Server: Docker Engine - Community
#  Engine:
#   Version:          18.09.2
#   API version:      1.39 (minimum version 1.12)
#   Go version:       go1.10.6
#   Git commit:       6247962
#   Built:            Sun Feb 10 00:13:06 2019
#   OS/Arch:          linux/amd64
#   Experimental:     true

Persistence Data

永続化が必要なデータは、/var/lib 以下にまとめられている。
/var/lib には、/dev/sda1 が mount されている。

$ mount -l | grep /var/lib
# /dev/sda1 on /var/lib type ext4 (rw,relatime,data=ordered)
# ...

$ ls -l /var/lib
# total 1048636
# drwxr-xr-x    5 root     root          4096 Feb 18 05:39 cni
# drwx------    9 root     root          4096 Feb 18 05:22 containerd
# drwx--x--x   15 root     root          4096 Feb 25 02:51 docker
# drwxr-xr-x    3 root     root          4096 Feb 20 08:58 dockershim
# drwxr-xr-x    3 root     root          4096 Feb 22 02:40 etcd
# drwxr-xr-x    3 root     root          4096 Feb 20 08:59 kubeadm
# drwx------    9 root     root          4096 Feb 20 08:58 kubelet
# drwxr-xr-x    3 root     root          4096 Feb 18 05:38 kubelet-plugins
# drwxr-xr-x    4 root     root          4096 Feb 22 04:55 log
# drwx------    2 root     root         16384 Feb 18 05:22 lost+found
# drwxr-xr-x    3 root     root          4096 Feb 18 05:22 nfs
# -rw-------    1 root     root     1073741824 Feb 25 02:50 swap

Volume Sharing

Docker for Windows で Shared Driver に設定されたドライブは自動で共有フォルダとなる。

File 共有に出された Drive は、Linux 側で /host_mnt/* というパスに変換されて mount される。
( 多分 Docker Client が勝手に Path 変換をしているんだろうと予想 )

その実態は、services.linuxkit/docker コンテナ内の /host_mnt/* に CIFS で mount される。

$ ctr --namespace services.linuxkit tasks exec --exec-id 1000 docker mount -l | grep host_mnt
# //10.0.75.1/C on /host_mnt/c type cifs (rw,relatime,vers=3.02,sec=ntlmsspi,cache=strict,username=<<Windows User>>,domain=<<Windows PC Name>>,uid=0,noforceuid,gid=0,noforcegid,addr=10.0.75.1,file_mode=0755,dir_mode=0777,iocharset=utf8,nounix,serverino,mapposix,nobrl,mfsymlinks,noperm,rsize=1048576,wsize=1048576,echo_interval=60,actimeo=1)

docker-Page-3 (2).png

Network

dockerd Container は LinuxKit Host の Default Network Namespace と同じ Namespace が割り当てられているので、以降は LinuxKit Host のネットワーク環境として見ていく。

現在、おおよそ以下の NIC が存在している。

Host Namespace

NIC Name IP master Default
Route
lo 127.0.0.1/8
eth0 192.168.65.3/28
hvint0 10.0.75.2/24
docker0 172.17.0.1/16
vethXXXXXXXXX@ifXXX docker0
cni0 10.1.0.1/16
vethXXXXXXXXX@eth0 cni0
tunl0@NONE
ip6tnl0@NONE

Container Namespace

NIC Name IP master
lo 127.0.0.1/8
eth0@ifXXX 172.17.X.X/16
tunl0@NONE
ip6tnl0@NONE

Interface: eth0

一見、一番簡単そうに見えて一番難しい NIC。
Linuxkit の Default Network Namespace の Default Route デバイス。
docker-Page-11.png
192.168.65.0/28 には、Default Gateway である 192.168.65.1 と、Windows Host を示す 192.168.65.2 がある。

$ ip n show dev eth0
# 192.168.65.1 lladdr f6:16:36:bc:f9:c6 ref 1 used 0/0/0 probes 1 REACHABLE
# 192.168.65.2 lladdr f6:16:36:bc:f9:c6 used 0/0/0 probes 4 STALE

一見すると物理 NIC にも見えるが、実は TAP 仮想デバイスであり、その裏では vpnkit というツールが Hyper-V Socket, vsock を利用して通信のトンネリング・仲介をしている。詳細な原理は後述。

Interface: hvint0

Docker Desktop は導入時、DockerNAT という仮想 Switch を作る。
LinuxKit はその DockerNAT に接続された状態で起動される。

Windows 側には イーサネット アダプター vEthernet (DockerNAT) という仮想 NIC が作成され、LinuxKit 側には hvint0 という NIC が作られ ( 正確には、起動時に eth0 だった物理 NIC をリネームしている )、どちらも DockerNAT に接続される。
docker-Page-2 (2).png
この経路は主にドライブの mount 用に利用されるようだ。

Network: docker0, vethXXXX@ifXXX

Docker が構築するいつものネットワーク。
各 Container は、Host とは違う Network Namespace をそれぞれ持つ。
veth のペアは、一つは Host Namespace に、もう一つは各 Container Namespace に配置される。

docker0 は bridge であり、veth に master としてリンクされている。
また、docker0 は IP Address も持っており、各 Container Namespace の Default Gateway となっている。

また iptables の IPマスカレード機能により、docker0 を通る Container の Outbound Packet 全て送信元 IP 変換がなされる。

docker-Page-4 (2).png

Network: cni0, vethXXXX@eth0

CNI プラグインで利用されるネットワーク。Kubernetes が有効になっていると作成される。

CNI ( Container Network Interface ) とは、Container の Networking を担当するプラグインの I/F 仕様。
多くの Container Runtime や Orchestrator が登場する中、各社独自の Networking 実装による重複を避ける目的がある。

各 Pod は、Host とは違う Network Namespace をそれぞれ持つ ( Pod 内の Container は同じ Network Namespace )。
今回は具体的な CNI プラグイン実装が入っていないが、例えば Flannel 等でクラスタが構築されれば多分以下のようになるはず。

docker-Page-5.png

今回は Kubernetes は射程外なので ( というか、自分自身が詳しくもないので ) あまり踏み込まない。

Tunnel: tunl0@NONE, ip6tnl0@NONE

稀に遭遇する謎のデバイス。一体何のためにあるのか分からなかった。
ちなみに、Container の中にもいる。

$ ip tunnel show
# tunl0: unknown/ip  remote any  local any  ttl inherit  nopmtudisc

$ ip addr show tunl0
# 3: tunl0@NONE: <NOARP> mtu 1480 qdisc noqueue state DOWN qlen 1
#    link/ipip 0.0.0.0 brd 0.0.0.0
$ ip link set dev tunl0 up
$ ip addr show tunl0
# 3: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN qlen 1
#     link/ipip 0.0.0.0 brd 0.0.0.0

$ ping -I tunl0 172.17.0.2
# PING 172.17.0.2 (172.17.0.2): 56 data bytes
# .
# .
# .
# ( 沈黙 )

calico の Github Issues にある情報だが、IPIP カーネルモジュールが読み込まれたときの副作用で作られるとの情報あり。

Docker 公式にもしれっと居たりする。そして触れられないという。

screenshot_37.png
https://docs.docker.com/network/none/

Host-Guest 間 socket 通信

古くは VMWare の VMCI Socket、最近では Qemu で使われる virtio-vsock ( Address-Fammily = AF_VAOCK ) という技術を使うことで、Network を一切介さずに VM Guest と Host の間で通常の BSD socker API を使った通信が可能となる。
メモリを共有し、その上でデータ交換するので高速な通信が可能となる。

https://medium.com/@mdlayher/linux-vm-sockets-in-go-ea11768e9e67
https://pubs.vmware.com/vsphere-51/index.jsp?topic=%2Fcom.vmware.vmci.pg.doc%2FvsockAbout.3.2.html
https://wiki.qemu.org/Features/VirtioVsock

そして 2017 年、ついに Hyper-V にもこの Host-Guest 間 socket 通信ができる機能が追加された。
Docker Desktop では至る所でこの Hyer-V Socket が利用されている。

Hyper-V Socket

Hyper-V Host と Guest との間で通信を行う Socket。2017 年頃に Windows 10, Windows Server 2016 に導入された。
Network を介さず、VMBus 経由でやり取りするのでハイパフォーマンス。
https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service

● Socket Address Family

この Socket を実現するため、socket のアドレスファミリに AF_HYPERV が追加された
Linux Guest 側は vsock を利用する。

● Guest Communication Service

Hyper-V Socket を利用するには、まずは Windows に Guest Communication Service というものを登録する必要がある。
これは、Unix Domain Socket で言うところの File Path のような、通信チャンネルの識別子的なもので、Windows Host の Registry に登録される。

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices にある。

見てみると、既に Docker や Kubernetes 関連のサービスがいくつか登録されているのが分かる。
screenshot_35.png

● Service GUID 命名規則

Hyper-V Socket と vsock では通信先アドレスの指定方法が違っていて、Hyper-V Socket の場合は VM GUIDService GUID を指定するが、vsock の場合は cidport ( 0 ~ 0x7FFFFFFF の数値 ) を指定する。

これらを両立させる為に、Service ID としての GUID を決める際には以下のルールに則る。

[[ Port Number ]]-FACB-11E6-BD58-64006A7986D3

例えば、Service ID 00000948-FACB-11E6-BD58-64006A7986D3 ( ElementName : Docker API ) について通信したい場合、以下の様な設定になる。

  • Hyper-V Socket
    • VM GUID
      • (Get-VM -Name 'DockerDesktopVM').Id
    • Service GUID
      • 00000948-FACB-11E6-BD58-64006A7986D3
  • vsock

docker-Page-6.png

また、Docker Desktop エコシステム中で利用される場合には、[[Protocol]]://[[VM ID]]/[[SERVICE ID]] のような Path 表記もされる。

# 30D48B34-FACB-... サービスについて、全ての VM からの接続要求を待つ 
hyperv-listen://00000000-0000-0000-0000-000000000000/30D48B34-FACB-11E6-BD58-64006A7986D3

# 0000F3A5-FACB-... サービスについて、全ての VM からの接続要求を待つ 
hyperv-listen://00000000-0000-0000-0000-000000000000/0000F3A5-FACB-11E6-BD58-64006A7986D3

# 0000F3A5-FACB-... サービスについて、VM (GUIT: ABCDEFGH-IJKL-...) に接続する
hyperv-connect://ABCDEFGH-IJKL-MNOP-QRST-UVWXYZZZZZZZ/0000F3A5-FACB-11E6-BD58-64006A7986D3

vsudd & com.docker.proxy.exe

Docker API 通信を Hyper-V socket で Tunneling して docker.sock へと Proxy するサービス。
これにより、Windows Host から dockerd が操作できる。
https://github.com/linuxkit/virtsock/tree/master/cmd/vsudd
docker-Page-8 (3).png

LinuxKit 上で起動した vsudd は、vsock(cid=VMADDR_CID_ANY, Port=00000948) で待ち受けて、受け取ったデータを docker.sock Unix Domain Socket へと Proxy する。

Windows Host 側では、サービスにより起動された com.docker.proxy.exe が Named Pipe //./pipe/docker_engine で待ち受けて、受け取ったリクエストを hyperv-listen://XXXXXXXX-XXXX-.../00000948-FACB-... 宛に転送する。

Docker Client から dockerd 宛に指示を出す時は、docker -H npipe://./pipe/docker_engine ~ となる。

VPNKit

Hyper-V socket/vsock を利用して様々な通信の仲介・Tunneling をするための Toolkit。 OCaml, Go, C で実装されている。
https://github.com/moby/vpnkit/

以下、主要なサービス。

  • On Linux Guest
    • vpnkit-tap-vsockd
      • Guest Communication Service : Docker VPN proxy ( vsock port : 0x30D48B34 )
      • Container, LinuxKit Host から外部ネットワークへの通信経路を提供
      • TAP デバイス eth0 を設置
      • eth0 ( vpnkit-tap-vsockd ) ⇔ vpnkit.exe を Hyper-V socket で Tunneling
    • vpnkit-forwarder
      • Guest Communication Service : Docker port forwarding ( vsock port : 0x0000F3A5 )
      • Windows Host から Linxkit Host への Port Forwarding 機能を提供
      • vpnkit.exevpnkit-forwarder を Hyper-V socket で Tunneling
      • vpnkit-forwarderContainer 間の Forwarding には、vpnkit-expose-port という別の担当がいる
      • Port が Leak しないように、9p filesystem ベースの管理を行う
        • Linuxkit 起動時に 9p filesystem を mount するのは vpnkit-9pmount-vsock が行う
  • On Windows Host
    • vpnkit.exe
      • vpnkit-tap-vsockd からの Frame を受け取り、Ethernet に流す
        • hyperv-listen://00000000-0000-0000-0000-000000000000/30D48B34-FACB-11E6-BD58-64006A7986D3
      • vpnkit-expose-port からの Port Forward 要求をうけとり、可否を返す。可ならその Port で自身が Listen。
        • hyperv-listen://00000000-0000-0000-0000-000000000000/0000F3A5-FACB-11E6-BD58-64006A7986D3
      • Port を Listen し、受け取った Packet を connect 先の vpnkit-forwarder に流す
        • hyperv-connect://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/0000F3A5-FACB-11E6-BD58-64006A7986D3

● vpnkit-tap-vsockd

LinuxKit → Windows で Ethernet over vsock/Hyper-V socket Tunneling を構築し、Container 内から Windows Host や Internet への通信を実現するサービス。
https://github.com/moby/vpnkit/blob/master/docs/ethernet.md
https://github.com/moby/vpnkit/tree/master/c/vpnkit-tap-vsockd
docker-Page-7.png
LinuxKit 内で外向け Packet が Default Route の eth0 に到着すると、vpnkit-tap-vsockd はそれを読み取り、Encapsulation して vsock(cid=VMADDR_CID_HOST, Port=30D48B34) へ向けて送信する。

vpnkit.exehyperv-listen://00000000-0000-.../30D48B34-FACB-... で待ち受けており、受け取ったデータを Decapsulation し、vpnkit.exe プロセス内部に持っている仮想 L3 Switch へと送る。
vpnkit は、送信先毎に 仮想 TCP/IP endpoint を作成しており、これが Transport Layer ( L4 ) Proxy として TCP/UDP Flow を終端する。
内部 Switch はこの仮想 TCP/IP Endpoint に対し 1 つの Switch Port を接続しておき、送信先で判定し Filtering する。
もし知らない送信先が来た場合、新たに仮想 TCP/IP Endpoint が作られ、新しい Switch Port が作成 & 接続される。

これらは全て vpnkit.exe プロセス内部で起こることで、Windows Host Kernel からは vpnkit.exe が複数の相手と socket 通信しているようにしか見えない。

● vpnkit-forwarder

Windows → LinuxKit で Port Forwarding を実現するサービス。
https://github.com/moby/vpnkit/blob/master/docs/ports.md
https://github.com/moby/vpnkit/tree/master/go/cmd/vpnkit-forwarder ( 元 proxy-vsockd )
https://github.com/moby/vpnkit/blob/master/go/cmd/vpnkit-userland-proxy ( 旧 slirp-proxy, 現 vpnkit-expose-port )
https://github.com/moby/vpnkit/tree/master/c/vpnkit-9pmount-vsock

前準備

まずは前準備として、vpnkit.exe 起動時に Port Forwarding 情報の共有のための 9p Server を立ち上げ hyperv-listen://00000000-0000-.../0000F3A5-FACB-... で待ち受ける。
Linuxkit 側では、onboot 時に vpnkit-9pmount-vsock Container が vsock(cid=VMADDR_CID_HOST, Port=0000F3A5) で接続し、その socket を Backend とした 9P filesystem を /port に mount する。

docker-Page-10.png

### `rfdno`, `wfdno` に設定されているのが、socket の file descriptor
$ ctr --namespace services.linuxkit tasks exec --exec-id 1000 docker mount -l | grep /port
# /port on /port type 9p (rw,relatime,sync,dirsync,trans=fd,dfltuid=1001,dfltgid=50,version=9p2000,msize=4096,rfdno=3,wfdno=3)

Port Forwarding

それでは、実際に Port Forwarding されるまでの一連の処理を見ていく。
docker-Page-9 (3).png

Container を立ち上げる。

PS> docker run -d -p 80:80 nginx

Docker Client から指示を受けた dockerd は、指定の IP, Port に対応した vpnkit-expose-port プロセスを Fork する。
vpnkit-expose-port は、指定した IP:Port で Listen し、これまた指定した Container へと転送する Forward Proxy だ。

$ ps | grep /usr/bin/vpnkit-expose-port
# 3404 root      0:00 /usr/bin/vpnkit-expose-port -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80

$ netstat -anp | egrep ':::80'
# tcp        0      0 :::80                   :::*                    LISTEN      3404/vpnkit-expose-

$ echo -en "GET / HTTP/1.0\n\n" | nc localhost 80 | grep 'Welcome to nginx!'
# <title>Welcome to nginx!</title>
# <h1>Welcome to nginx!</h1>

通常、Docker Daemon は iptables の NAT Table に Static な Forwarding 設定を追加する事で Port Forwarding を実現するが、起動時に --userland-proxy-path オプションを渡すことで、独自の Userland Proxy を使うようすることができる。
( とはいえ、互換性を考慮してか、現在は vpnkit-iptables-wrapper が代わりに呼ばれ、iptables を変更しつつ vpnkit-expose-port も起動するようだ )

$ ps | grep dockerd
# 1291 root      7:56 /usr/local/bin/dockerd -H unix:///var/run/docker.sock --config-file /run/config/docker/daemon.json --swarm-default-advertise-addr=eth0 --userland-proxy-path /usr/bin/vpnkit-expose-port

また、vpnkit-expose-port は起動時に /port 下に [Src Protocol]:[Src IP]:[Src Port]:[Dest Protocol]:[Dest IP]:[Dest Port] というフォルダを作成する事で、9p 経由で vpnkit.exe へと Port Forwarding 情報を伝える。

$ ctr --namespace services.linuxkit tasks exec --exec-id 1000 docker ls /port
# README
# tcp:0.0.0.0:80:tcp:172.17.0.3:80

Port Forwarding 情報を受けた vpnkit.exe は、自身が Port Forwarding するその Port で Listen し始める。

ここで、Windows Host から localhost:80 にアクセスすると、まず vpnkit.exe に connect され、vpnkit.exe 内で Multiplexing, Encapsulation されて hyperv-connect://<<DockerDesctopVM>>/0000F3A5-FACB-... へ向けて送信される。

vpnkit-forwardervsock(cid=VMADDR_CID_ANY, Port=0000F3A5) で待ち受けており、受け取ったデータを Decapsulation, Demultiplexing し、後は Forward Proxy として [Dest IP]:80 にアクセスする。

( ん、Dest IP 指定するなら vpnkit-expose-port の Listen 要らないのでは ? ここ とか ここ とか ここ 見ると Dest IP 教えてるっぽい )

ちなみに 9p をわざわざ使っているのは、vpnkit-expose-port が起動中 /port/XX:XX:XX:XX:XX:XX File Descriptor をわざと Open したままにしておくことで、Crush や Kill された際に 9p の clunk Message が vpnkit へ通知され、Leak を防ぐことができる為らしい。

● Windows Named Pipe

Windows には、Named pipe ( 日本語で、名前付きパイプ ) と呼ばれるプロセス間通信の方法がある。
Unix にも同名の概念があるが、Windows の場合は以下の特徴がある。

  • ファイル実体はなく、NPFS ( named pipe filesystem ) 上に mount される
    • \\.\pipe\PipeName
  • 揮発性で、通信プロセスが止まれば消える
  • Windows で Unix Domain Socket の代わりとして選択されるケースが多い

以下、分かる範囲で見ていく。
image.png

● \\.\pipe\docker_engine
com.docker.proxy が Docker API Call を待ち受けている Named Pipe。
Docker Client が繋ぎに行っている。

\\.\pipe\docker_engine_windows というのもあるが、こっちは Windows の dockerd へと繋がっている。

PS> docker -H "npipe:////./pipe/docker_engine" info | wsl grep OSType
# OSType: linux
PS> docker -H "npipe:////./pipe/docker_engine_windows" info | wsl grep OSType
# OSType: windows

● \\.\pipe\dockerVpnKitControl
vpnkit.exe 起動時に、9p Control 用待受アドレスとして渡される 2 つのアドレスの内の 1 つ。

vpnkitexe起動パラメータ
vpnkit.exe .... --port //./pipe/dockerVpnKitControl --port hyperv-listen://00000000-0000-0000-0000-000000000000/0000F3A5-FACB-11E6-BD58-64006A7986D3 .....

通常は、hyperv-listen://00000000-0000.../0000F3A5-FACB-... 経由で操作されるはずだが、誰か Windows 側でも繋いでいるのかもしれない。

● \\.\pipe\dockerVpnKitDiagnostics
vpnkit.exe 起動時に診断用待受アドレスとして渡される。

vpnkitexe起動パラメータ
vpnkit.exe ..... --diagnostics \\.\pipe\dockerVpnKitDiagnostics ....

多分 ここ に書かれている診断用データを流すための Named Pipe と思われる。

The active ports may be queried by connecting to a Unix domain socket on the Mac or a named pipe on Windows and receiving diagnostic data in a Unix tar formatted stream.

試しに繋いでみると、すごい勢いで謎の Binary ( 多分 Tar 圧縮されている ) が流れてくる。

● \\.\pipe\dockerLogs
Windows 側で Log を集約するための Endpoint と予想。
送ってみたが接続数限界らしい。なので未確認。

$ echo 'hoge' > \\.\pipe\dockerLogs
# out-file : すべてのパイプ インスタンスがビジーです。
# 発生場所 行:1 文字:1
# + echo hoge > \\.\pipe\dockerLogs
# + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#     + CategoryInfo          : OpenError: (:) [Out-File], IOException
 #    + FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.OutFileCommand

● \\.\pipe\dockerDockerDesktopVM-com1
名前の通りなら COM Port。
のはずだけど、VM の設定見ても COM Port 無いんだよなぁ。謎。

● DNS

Docker Desktop によって、C:\Windows\System32\drivers\etc\hosts に以下が追加されている。
IP ADDRESS の部分には、Host の Default Route の IP が入っている。

hosts
...

# Added by Docker Desktop
[[IP ADDRESS]] host.docker.internal
[[IP ADDRESS]] gateway.docker.internal
# End of section

ただ、Wifi の繋ぎ直し等をして Network 環境が変わっても書き換えられない。

Linuxkit 側では、192.168.65.0/28 の Default gateway と Windows Host と思しき相手が設定されている。
どちらも vpnkit-tap-vsockd の作る仮想的な Network 内の Node だ。

$ nslookup gateway.docker.internal
# nslookup: can't resolve '(null)': Name does not resolve
#
# Name:      gateway.docker.internal
# Address 1: 192.168.65.1

$ nslookup host.docker.internal
# nslookup: can't resolve '(null)': Name does not resolve
# 
# Name:      host.docker.internal
# Address 1: 192.168.65.2


$ ip a show dev eth0
# 5: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000
#     link/ether 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff
#     inet 192.168.65.3/28 brd 192.168.65.15 scope global eth0
#        valid_lft forever preferred_lft forever
#     inet6 fe80::50:ff:fe00:1/64 scope link
#        valid_lft forever preferred_lft forever
$ ip n show dev eth0
# 192.168.65.1 lladdr f6:16:36:bc:f9:c6 ref 1 used 0/0/0 probes 1 REACHABLE
# 192.168.65.2 lladdr f6:16:36:bc:f9:c6 used 0/0/0 probes 4 STALE

● Diagnosis

Log : Docker Desktop

Docker Desktop が出しているログ。
C:\Users\username\AppData\Local\Docker 以下に出力される。
以下、代表的な出力元。

● Moby
Linuxkit カーネルのログと LinuxKit の初期化処理のログが出力されている。
どの経路で Linux から Windows 側に送られているのかは知らない。

● VpnKit
vpnKit.exe のログ。LinuxKit 側の forwarder 等のログは無いようだ。

● HyperV
Hyper-V の操作ログ。

● ApiProxy
com.docker.proxy.exe のログと思われる。主に Linux 側の Docker Daemon への指示とその返信が出力される。

● NamedPipeServer/NamedPipeClient
ログを見ると、バージョンを送ったり、VM のディスクサイズを送ったり、engine スタートしろと指示を出したりしている。
重要な仕事をしてそうなのだが、誰が Server で誰が Client なのか不明。

Log : LinuxKit

LinuxKit のログ。
普通に LinuxKit Host の /var/log 以下にある。
OS は Read-Only のはずだが、/var/log/var/lib/log の Alias になっている。

まとめ

Docker + Kubernetes 環境となると、どうしても L2 ~ L3 辺り動的でかつ複雑になるのは避けられなくて、そんな中でも確実に通信経路を確保するためには、やはり Unix Domain Socket や Named Pipe の様なプロセス間通信が有効になるのかなと思いました。

Docker の情報というと、入門と How To と Linux 要素技術との関係性が多いので、少し違う視点からのまとめとしても役に立てば良いなぁと思います。

次回に続く。

おまけ

NIC が、物理 NIC なのか、Bridge なのか、TUN/TAP なのか、ethtool が無い環境でどう調べる方法

  • Physical devices - /sys/class/net/eth0/device があるかどうか
  • Bridges - /sys/class/net/br0/bridge があるかどうか
  • TUN and TAP devices - /sys/class/net/tap0/tun_flags があるかどうか

参考 : How to know if a network interface is tap, tun, bridge or physical?

Kubernetes が起動しない

Error while setting up kubernetes: cannot update the host kube config: Failed to load Kubernetes CA: couldn't load the certificate file C:\ProgramData\DockerDesktop\pki\ca.crt: open C:\ProgramData\DockerDesktop\pki\ca.crt: Access is denied

一旦 Windows のエクスプローラで C:\ProgramData\DockerDesktop\pki\ を開くと『このフォルダにアクセスする権限がありません』が出るので、これで『続行』を押せば、それ以降アクセスできるようになる。

Error while setting up kubernetes: cannot update the host kube config: cannot load current kubernetes config: Error loading config file \"C:\Users\username\.kube\config\": yaml: control characters are not allowed.

C:\Users\username.kube\config を一旦リネームすると、新たに作り直されて解決する。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40