以前、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. 用語の整理
Linux Container
Linux Kernel で動作する Container のこと。
Windows Container
Windows の NT Kernel で動作する Container のこと。
場合によって呼び方は異なるが、多分公式にもこう呼ばれているはず。
Windows Server Container
Windows Container の実現方法の 1 つ。
Process レベルで分離される。
Windows process container とも呼ばれる。
Hyper-V Container
Windows Container の実現方法の 1 つ。
kernel レベルで分離される。
Windows Hyper-V container とも呼ばれる。
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.yml
の kernel
→ init
→ onboot
→ onshutdown
→ services
→ files
セクションの順に実行される。全ての処理が、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)
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 デバイス。
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
に接続される。
この経路は主にドライブの 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 変換がなされる。
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 等でクラスタが構築されれば多分以下のようになるはず。
今回は 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 カーネルモジュールが読み込まれたときの副作用で作られるとの情報あり。
- tunl0 device present inside containers - github.com/projectcalico/calicoctl
- tun10@NONE interface inside the pods is of no use - github.com/projectcalico/calico
- what does none in “tun0@none” stand for? - Unix & Linux Stack Exchange
Docker 公式にもしれっと居たりする。そして触れられないという。
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 関連のサービスがいくつか登録されているのが分かる。
● Service GUID 命名規則
Hyper-V Socket と vsock では通信先アドレスの指定方法が違っていて、Hyper-V Socket の場合は VM GUID
と Service GUID
を指定するが、vsock の場合は cid
と port
( 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
- VM GUID
- vsock
- cid
-
VMADDR_CID_HOST
( これ一択 ? )
-
- port
- 0x00000948 →
2376
- 0x00000948 →
- cid
また、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
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.exe
⇔vpnkit-forwarder
を Hyper-V socket で Tunneling -
vpnkit-forwarder
⇔Container
間の Forwarding には、vpnkit-expose-port
という別の担当がいる - Port が Leak しないように、9p filesystem ベースの管理を行う
- Linuxkit 起動時に 9p filesystem を mount するのは
vpnkit-9pmount-vsock
が行う
- Linuxkit 起動時に 9p filesystem を mount するのは
-
vpnkit-tap-vsockd
- 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.exe
● 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
LinuxKit 内で外向け Packet が Default Route の eth0
に到着すると、vpnkit-tap-vsockd
はそれを読み取り、Encapsulation して vsock(cid=VMADDR_CID_HOST, Port=30D48B34)
へ向けて送信する。
vpnkit.exe
は hyperv-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 する。
### `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 されるまでの一連の処理を見ていく。
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-forwarder
が vsock(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 の代わりとして選択されるケースが多い
● \\.\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 つ。
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
起動時に診断用待受アドレスとして渡される。
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 が入っている。
...
# 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 を一旦リネームすると、新たに作り直されて解決する。
参考