20190318のdockerに関する記事は16件です。

[Docker]アラフォーがDockerに挑んでみた(黎明編)

はじめに

業務でDockerが絡むシステムを担当することになってしまい、「現場入ってから追々理解してもらえばだいじょうぶっすー」という言葉を信じていたのだけど、「追々じゃなく今理解せんとこれ開発出来んやん」ということに気付いたので今更ながらDockerの扉を叩くことにしました。
アラフォーの日々少なくなってゆく脳細胞で理解出来るでしょうか。

なぜ今Dockerなのか(私の場合)

現場の方からは「アプリ単位のVMみたいなもんやと思ってもらえればー」と聞いていた。ふーん、そっかぁ。そういうもんがあるんやな、ぐらい知っとけばいいんかな。と思ってました。
ところが、いざ実作業に入ると確かにDockerをよく知らなくても、既存のAPIなりを呼んでいけば仕様を満たす実装は出来る。
けどテストクラス書くときに

「これってbeforeでDockerの状態をまずお膳立てしてやらんとテストしようがないけどどうなってるのが正しいのか・・・Dockerの何をどう見てassertTrueとかするの・・・」

というところで唸ることになってしまった。これはマズい。Dockerってなんやのん、ということを知る必要が出てきてしまいました。
つまり、使いたくなった!とかそういうわけではなく、業務上困り始めたし、そんならやってみるかーって流れです。

あしたっていまさッ!

ジョジョの奇妙な冒険のポコという登場人物が「あしたっていまさッ!」という名言を残しています。
私がDockerを勉強するのも今なんでしょう。先延ばしにしてたけど。幸い酒飲んでアニメ見てソシャゲする時間の余裕はあります。やろうではないですか。

Dockerってなんやのん

いきなり小難しいサイトや本を読んでもきっと頭に入らないので、まずはやんわり気味に情報を入手していくことにしました。
Dockerのすべてが5分でわかるまとめ!(コマンド一覧付き)

うーん、でVMと何が変わらないんだ?
アプリ単位でのイメージを切り出せるから、開発環境が作りやすいというのは何となくわかる。
とりあえず使ってみますか。

Dockerをインストールしてみる

必要なもの

  • PC
  • ネット環境
  • メールアドレス
    以上であります。因みに今回使用したマシンのスペックは下記。
  • macOS Sierra 10.12.6
  • MacBook (Retina, 12-inch, Early 2015)
  • プロセッサ 1.3 GHz Intel Core M
  • メモリ 8G

説明するまでもない手順

  • Dockerのサイトの、「Download from Docker Hub」と書いてるいかにもなボタンを押下。
    https://docs.docker.com/docker-for-mac/install/
    スクリーンショット 2019-03-18 21.40.30.png

  • ポチッとするとSign Inせよと言われるので、アカウントがない場合はCreate Accountのリンクからアカウントを作成。私はアカウントなかったので作りました。
    スクリーンショット 2019-03-18 21.41.28.png

  • と言ってもEメールとパスワードと、同意せよチェックボックスだけなので、入力してとりあえずなんでも同意マンになろう。
    スクリーンショット 2019-03-18 21.41.44.png

  • ぼへっとしてたらメールが来るので、Confirmマンになってアカウントを認証。
    スクリーンショット 2019-03-18 21.42.45.png

  • 上のメニューのとこからSign Inすると、Get Dockerボタン押下でdmgファイルダウンロード出来ます。やったね。
    スクリーンショット 2019-03-18 21.43.12.png

  • dmgファイルをポチっとするとこういうのが出るので、おとなしく従う。
    スクリーンショット 2019-03-18 21.51.39.png

  • いかにもなログイン画面らしきものが表示されるので、登録したアカウントでログイン。
    スクリーンショット 2019-03-18 21.53.26.png

  • ログイン後はこんな感じに。何が変わったのやら?
    スクリーンショット 2019-03-18 22.13.09.png

  • これでインストール出来たのか不安になったので、docker versionをターミナルで叩いてみる。

$ 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 04:12:39 2019
 OS/Arch:           darwin/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 04:13:06 2019
  OS/Arch:          linux/amd64
  Experimental:     false

何やらインストールは出来てるみたいですね!

ついでなのでPythonもインストールしてみよう

Dockerのウリとして、アプリケーションなどの実行環境や設定方法をまとめて1つのパッケージにし、それを「Dockerイメージ」として保存・配布しているそうです。
なるほど、それが現場の方の「アプリ単位のVMみたいなもんやと思ってもらえればー」ってとこに繋がるのかな。
今現場で使っているPythonが3.6なので、自宅学習用にもせっかくなのでそれをインストールしてみます。
ちなみに素のMacBookにはPython2.7.10がインストールされている状態ですので、Docker上にPython3.6をインストール出来るなら、MacBookの環境を汚さない、ってことになるハズ。やってみましょう。
インストール前の状態がこちら。

$ python --version
Python 2.7.10

Python3.6のDockerイメージインストール

おもむろにsudo docker pull python:3.6をターミナルから実行します。自分しか使わないマシンなのに、sudoにパスワードかけてらっしゃる!?

$ sudo docker pull python:3.6
Password:
3.6: Pulling from library/python
22dbe790f715: Pull complete 
0250231711a0: Pull complete 
6fba9447437b: Pull complete 
c2b4d327b352: Pull complete 
270e1baa5299: Pull complete 
400bad26d6c0: Pull complete 
1d360410c080: Pull complete 
1224d2c3c8d9: Pull complete 
5d7d0629cdba: Pull complete 
Digest: sha256:dfd0b1fcad1f056d853631413b1e1a3afff81105a27e5ae7839a6b87389f5db8
Status: Downloaded newer image for python:3.6

終わったようなのでdocker image lsで見てみましょう。lsコマンド感覚で何インストールしてるのか見られるみたい。

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
python              3.6                 d6b15f660ce8        13 days ago       924MB

うん、Python3.6はインストール出来てる・・・みたい。

Dockerイメージの起動

docker run -it python:3.6 でPythonを起動してみます。

docker run -it python:3.6 
Python 3.6.8 (default, Mar  5 2019, 06:26:06) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.

無事Python3.6が起動してるみたいですね。
exitでDockerを停止した後、改めてpython --versionでバージョンを見てやると

python --version
Python 2.7.10

おー、元々のバージョンは何も変わってない。
これで「本体のMacBookに入ってるPython2.7.10」を汚すことなく、Python3.6(正確には3.6.8だけど)がDockerという仮想環境にインストール出来て、実行も出来る環境が出来たってことですね。
まとめを書きながら進めていたので時間はかかりましたが、単純にお手持ちのPCに導入するだけならここまででしたら1時間もかからないと思います。

ここから更にいじって遊んでみたいので、後日色々試していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vagrant/Docker】Windowsでも開発できるようにVagrant + Docker の環境を作る

Docker for WindowsVagrant/VM VirtualBoxが相容れない問題

VirtualBox 6.0 から Hyper-V と共存できるはず - Qiita
を見てやっと共存できるようになったんか~~と思って試してみたんですが、
やっぱダメだったんであきらめてVagrant上でDocker動かすことにしました。

Vagrant + Docker の環境を作っていく

前回Docker for Windowsで環境構築しましたが、今回はVagrant + Dockerで環境構築していきます。

実行環境

OS:Windows 10 Pro 64bit
RAM:8GB

Vagrantfileを用意する

以下の記事を参考にさせていただきました :bow_tone2:
参考:centos7を起動してすぐDockerできるVagrantfile - Qiita

ただ、記事に書いてある文のままだとDocker Composeのバージョンが古いので、
GitHubのReleases · docker/composeを 開き、最新のバージョンに書き換えました。

Vagrantfile
Vagrant.configure("2") do |config|

  config.vm.box = "centos/7"

  config.vm.network "forwarded_port", guest: 80, host: 80
  config.vm.network "private_network", ip:"192.168.77.11"

  config.vm.synced_folder "./", "/var/www"

  config.vm.provision "shell", inline: <<-SHELL
    sudo yum install -y yum-utils
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    sudo yum makecache fast
    sudo yum install -y docker-ce
    sudo systemctl enable docker-ce
    sudo systemctl start docker-ce
    sudo curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` > docker-compose
    sudo mv docker-compose /usr/local/bin/docker-compose
    sudo chmod +x /usr/local/bin/docker-compose
    sudo gpasswd -a vagrant docker
    sudo systemctl restart docker
  SHELL
end

vagrant upするとエラーが発生

vagrant upすると↓のようなエラーが発生。

Vagrant was unable to mount VirtualBox shared folders. This is usually
because the filesystem "vboxsf" is not available. This filesystem is
made available via the VirtualBox Guest Additions and kernel module.
Please verify that these guest additions are properly installed in the
guest. 

This is not a bug in Vagrant and is usually caused by a faulty
Vagrant box. For context, the command attempted was:

mount -t vboxsf -o uid=1000,gid=1000 var_www_html /var/www/html

The error output from the command was:

mount: unknown filesystem type 'vboxsf'

どうやらvboxsfがないらしい

ので上記のサイトを参考にプラグインvagrant-vbguestをインストール

$ vagrant plugin install vagrant-vbguest
Installing the 'vagrant-vbguest' plugin. This can take a few minutes...
Installed the plugin 'vagrant-vbguest (0.17.2)'!

完了したもよう

続いて再びvagrant up

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'centos/7' version '1902.01' is up to date...
==> default: Clearing any previously set forwarded ports...
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: You are trying to forward to privileged ports (ports <= 1024). Most
==> default: operating systems restrict this to only privileged process (typically

【--- 略 ---】

    default:                                  Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
100   617    0   617    0     0    197      0 --:--:--  0:00:03 --:--:--   197
  0     0    0     0    0     0      0      0 --:--:--  0:00:03 --:--:--     0
  1 8649k    1  100k    0     0  21775      0  0:06:46  0:00:04  0:06:42 94888
 27 8649k   27 2406k    0     0   430k      0  0:00:20  0:00:05  0:00:15 1233k
 40 8649k   40 3542k    0     0   526k      0  0:00:16  0:00:06  0:00:10 1149k
 51 8649k   51 4470k    0     0   577k      0  0:00:14  0:00:07  0:00:07 1091k
100 8649k  100 8649k    0     0  1024k      0  0:00:08  0:00:08 --:--:-- 1800k
    default: Adding user vagrant to group docker

完了

vagrant sshでssh接続

$ vagrant ssh
Last login: Thu Mar 14 02:04:26 2019 from 10.0.2.2
[vagrant@localhost ~]$ 

試しにdocker run hello-worldを実行

[vagrant@localhost ~]$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:2557e3c07ed1e38f26e389462d0377a99efb77324b0fe53577a99efb77324b0fe535
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

[vagrant@localhost ~]$

無事に動いた

【番外編】 docker-compose upする

※ここでは前回、【Docker】Windows10でPHP5.3の環境を作るためにDockerを使用した際の軌跡 - Qiitaで作成した
docker-compose.ymlと同じものを使用します。

docker-compose.ymlを置いたディレクトリへ移動後、docker-compose up

[vagrant@localhost ~]$ cd ../../var/www/html/
[vagrant@localhost html]$ docker-compose up
Creating network "html_default" with the default driver
Creating volume "html_mysql-db" with local driver
Pulling db (vsamov/mysql-5.1.73:latest)...
latest: Pulling from vsamov/mysql-5.1.73
30d541b48fc0: Pull complete

[~~~ 略 ~~~]

db_1   | 190314  3:11:18 [Note] mysqld: ready for connections.
db_1   | Version: '5.1.73'  socket: '/tmp/mysql.sock'  port: 3306  MySQL Community Server (GPL)
www_1  | httpd: Could not reliably determine the server's fully qualified domain name, using 172.18.0.3 for ServerName

動いた

上記で作成したVagrantfileだと http://192.168.77.11:8080でブラウザ上から確認できる

その他dockerコマンド

docker起動

[vagrant@localhost ~]$ service docker start

ちなみに次回起動時はservice docker startする必要がある

dockerが動いているか確認

[vagrant@localhost html]$ sudo service docker status

おわり

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでDeep Learningなどの環境をドカドカ構築

はじめに

やぁみんな!最近はAI人材になれば新卒でも年収1000万円も手が届くとかで,みんなDeep Learningに興味津々だね!
えっ?環境構築が難しくて手が出せない?

確かにGPUが絡んだ途端に周りのライブラリの整備とか無限に面倒だし,右も左もわからない人には環境構築がまず敷居が高くて手が出せないし,
そもそもネットに落ちてるサンプルとかはフレームワークそのものが違ったり,フレームワークのバージョンが違うのがいくつもあって,フレームワークの切り替えが大変で使いこなすのが難しいよね...

でも大丈夫!今時のDeep Learningフレームワークは公式でDocker Imageを配っていたり,GitHubでDockerfileを配っているのがほとんどだから,それをちょっと改変するだけで,あっという間に環境構築が出来るんだ!

最初の設定は,慣れてない人にはちょっと手こずるかもしれないけど,一度設定すればあらゆる環境をすぐに切り替えて動かすことが出来るよ!

必要な物(物理)

  • Nvidia GPUが入ったLinuxマシン
  • ストレージ(仮想環境をドカドカ立てるのであれば,自由に使える容量が100GBは欲しい)

環境構築手順

以下Ubuntuを前提にして話を進めます

GPUドライバーインストール

公式ページからダウンロード出来るので,それに実行権限を付けて実行しましょう.
通常以下のコマンドで大丈夫なはずです.

chmod +x (ダウンロードしたファイル)
sudo ./(ダウンロードしたファイル)

です.
インストール中は色々聞かれますが,適宜対処してください.

dockerインストール

aptでインストール...と言いたいところですが,aptで通常インストール出来る奴はバージョンが古かったりしてあまり推奨されてないので,公式に従ってインストールしましょう.
なお,デフォルトではdockerはsudoを付けないと動作しませんが,それが嫌って人は,自分のアカウントをdockerが所属するグループに紐づけましょう.

(参考)
Dockerコマンドをsudoなしで実行する方法

nvidia-dockerインストール

公式GitHubのQuickstartの所を実行してください.
なお,上記の方法でdockerをインストールした場合は最初から最新のdockerが入ってるはずなので,二番目のリポジトリを追加するところから始めて大丈夫です.

Docker Imageの用意

公式が配布するDocker Imageを入手する

メジャーなフレームワークだったら,Docker Imageが配布されている場合があるので,DockerfileをBuildする手間が省けます.
ただし環境をカスタマイズしたいなら,後述のようにDockerfileからビルドした方が良いです.

Tensorflowの例

  1. TensorflowのDockerHub にアクセスして,Tagsから欲しいバージョンのimageを見つけます.
  2. 次のコマンドを入力してdocker imageを落としてきます
docker pull tensorflow/(欲しいTagの名前)

(Docker Imageが無い場合)公式のGitHubのページにいってcloneする

だいたいのメジャーなDeepLearningフレームワークはGitHubのページがあるので,それをcloneしましょう

Pytorchの例

  • PytorchのGitHubにアクセスして,git cloneします.
  • docker/pytorchディレクトリに移って,Dockerfileを確認します.するとこのような中身があるはずです.
FROM nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04
ARG PYTHON_VERSION=3.6
RUN apt-get update && apt-get install -y --no-install-recommends \
         build-essential \
         cmake \
         git \
         curl \
         vim \
         ca-certificates \
         libjpeg-dev \
         libpng-dev &&\
     rm -rf /var/lib/apt/lists/*


RUN curl -o ~/miniconda.sh -O  https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh  && \
     chmod +x ~/miniconda.sh && \
     ~/miniconda.sh -b -p /opt/conda && \
     rm ~/miniconda.sh && \
     /opt/conda/bin/conda install -y python=$PYTHON_VERSION numpy pyyaml scipy ipython mkl mkl-include cython typing && \
     /opt/conda/bin/conda install -y -c pytorch magma-cuda100 && \
     /opt/conda/bin/conda clean -ya
ENV PATH /opt/conda/bin:$PATH
RUN pip install ninja
# This must be done before pip so that requirements.txt is available
WORKDIR /opt/pytorch
COPY . .

RUN git submodule update --init
RUN TORCH_CUDA_ARCH_LIST="3.5 5.2 6.0 6.1 7.0+PTX" TORCH_NVCC_FLAGS="-Xfatbin -compress-all" \
    CMAKE_PREFIX_PATH="$(dirname $(which conda))/../" \
    pip install -v .

RUN git clone https://github.com/pytorch/vision.git && cd vision && pip install -v .

WORKDIR /workspace
RUN chmod -R a+w /workspace

中を見れば大体何やってるかわかると思いますが,ここではapt-getで最小限の環境をそろえて,ビルド環境を整えて,condaをインストールしてからそこにpytorchをビルドしてインストールしてます.

あとは自分が欲しいものに応じてDockerfileを編集してください.

  • Dockerfileを以下のコマンドでbuildします.
 docker build -t (tagの名前) .

Dockerfileがあるディレクトリでビルドすることに気を付けてください.
大抵のDeepLearningフレームワークでは,Dockerfileが存在するディレクトリでビルドすることが想定されています(それ以外でビルドしたらエラーが起こった)

docker-compose.ymlの記述

ビルド出来たらいよいよコンテナを起動して使う...前にdocker-composeでコンテナを簡単に扱いやすくしましょう.
以下がtensorflowのdocker-composeの例です.

docker-compose.yml
version: '2.3'
services:
    tensorflow:
        container_name: tensorflow_1
        image: tensorflow/tensorflow:latest-gpu-py3-jupyter
        volumes:
            - /path/to/data:/mnt
        runtime: nvidia
        tty: true
        command: /bin/bash
        ports:
            - 8889:8889

詳しい解説は他の物に譲りますが,ここで設定しているのは以下のようになります.

  • version: docker-composeのバージョン.古いとnvidia-dockerが動かないので,2.3を指定しています.
  • services:tensorflow: サービスの名称を決めます.ここ被ると他のdocker-composeで操作されるので被らないようにしましょう
  • container_name:コンテナ名を決める.docker psなどで確認したりする際に重要
  • image:DockerHubから取ってきたりビルドしたりしたimageファイルを指定
  • volumes:Dockerホストとのデータをやり取りする場所を指定.大体の環境では/mntがあるので,自分はいつも,Dockerホストの共有したいフォルダを/mntにマウントしてます
  • runtime: nvidia-dockerを動かすために必要なおまじない
  • tty: true :ここfalseだと,dockerが起動即終了するので,立ち上げてすぐプログラムを走らせて終了,みたいな使い方じゃなければ,trueにして,あとからexecコマンドで動作させましょう.
  • command: 起動した際に走らせるコマンドです.
  • ports:Dockerホストとつなげたいポートです.

起動&実行

以上までで環境構築及びdocker-compose.ymlの記述が終わったら,ymlファイルがあるところに移動して次のコマンドでコンテナを起動してください.

docker-compose up -d

するとコンテナが起動するので,psコマンドで以下のように出るはずです.

docker ps

CONTAINER ID        IMAGE                                          COMMAND                CREATED              STATUS              PORTS                              NAMES
89a5fb55012c        tensorflow/tensorflow:latest-gpu-py3-jupyter   "/bin/bash"            3 days ago          Up 3 days           8888/tcp, 0.0.0.0:8889->8889/tcp   tensorflow_1

このコンテナに対してtensowflowを行う場合は,以下のexecコマンドでコンテナの中でbashを起動します

docker exec -it 89a5 /bin/bash

ここでは,IDが89a5fb55012cなので,-itの後に89a5とこのIDを特定できる文字列を渡せばそのコンテナの中に入れます.
あとは,中にtensowflowの環境が用意されているので,思いっきりDeepLearningをするだけです!

DockerHubにimageがある著名なDeepLearningフレームワーク例

著名なDeepLearningフレームワークにはDockerHubにimageが転がってきて,しかも大体最新のバージョンのimageが直ぐに配布されるので,その中から使いたいものを使いましょう

GitHubにDockerfileがあるフレームワーク(DeepLearningとは限らない)例

DockerHubに公式のものが無い場合は,GitHubに転がってるDockerfileを使うのがオススメです.
他人が作ったImageもあることにはありますが,公式で配布されているものを使った方が確実だったり,自分好みの環境を整えたりと考えると自前でbuildした方が良い気がします
-WebDNN
ブラウザ上でDeepLearningが出来るようになるフレームワーク.使うためにはサーバーの設定とかややこしい設定がいくつかありますが,Dockerでなら一発で出来ます
-LightGBM
DeepLearningではないですが,Kaggleなどのコンペで用いられる決定木ベースのアンサンブル学習の実装です.GPU周りの設定などもこのDockerfileを使えば一発でわかります.

終わりに

いかがでしたでしょうか.機械学習関係は時代が進むにつれてどんどん新しいものが出てくるため,一つの環境に色々と混在させているとやっかいなことになりがちですが,Dockerを使えば様々な環境を同時に便利に扱うことが出来ます.Dockerはもはや機械学習関係のことを行うのに必須な環境と言ってよいでしょう.みなさんの健闘をお祈りします.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerコンテナ上でJavaプログラムを動かすときにLANG環境変数を設定すると日本語のファイル名が文字化けする問題

概要

CentOS の Docker コンテナ上で Java プログラムを動かしていたところ、日本語のファイル名を含むファイル一覧の取得で謎の文字化けが発生しました。

Sample.java
import java.io.*;

public class Sample {
   public static void main(String[] args) {
      // ファイル名が日本語のファイル「/sample/あいうえお.csv」を配置しておく
      new File("/sample").listFiles(new FilenameFilter() {
         public boolean accept(File dir, String name) {
            System.out.println(name);   // => ファイル一覧を取得すると日本語ファイル名が文字化けする
            return false;
         }
      });
   }
}

ちなみに、LANG環境変数を en_US.UTF-8 とした場合は文字化けが発生せず、 ja_JP.UTF-8 とした場合は文字化けが発生することが確認できています。

本記事では日本語ファイル名の文字化けの原因と対処方法について記載します。

原因と対処方法

まず、LANG環境変数へ ja_JP.UTF-8 を設定すると文字化けが発生する原因ですが、これは Docker の CentOS イメージに日本語ロケールが登録されていないため です。

LANG環境変数に指定可能なロケールについては locale -a コマンドから確認することができます。
CentOS イメージのコンテナ内でコマンドを実行して確認してみます。

# locale -a
C
POSIX
en_US.utf8

上記の通り、Docker の CentOS イメージのコンテナ内には日本語ロケールが含まれていません。
このコンテナ内で以下のようにLANG環境変数を指定してJavaプログラムからファイル一覧を取得しようとすると日本語ファイル名の文字化けが発生します。

LANG=ja_JP.UTF-8
export LANG

java Sample
=> 文字化けした日本語ファイル名.csv

対処方法として、 localedef コマンドを使用して 日本語ロケールを追加する ことで文字化けが解消します。
以下のコマンドを Dockerfile の RUN 命令として追加するかコンテナ内で実行します。

# localedef -f UTF-8 -i ja_JP ja_JP.UTF-8

もう一度 locale -a コマンドで指定可能なロケールを確認してみます。

# locale -a
C
POSIX
en_US.utf8
ja_JP.utf8

localedef コマンドによって ja_JP.utf8 が追加されました。
これでLANG環境変数を設定した場合も文字化けすることなく日本語のファイル名を扱えるようになります。

結論

  • Docker の CentOS イメージのコンテナ内には日本語ロケールが含まれていない
  • 日本語ロケールは localedef コマンドで追加できる
  • 指定できない(環境に存在しない)ロケールをLANG環境変数から指定するとJavaプログラムで日本語ファイル名が文字化けする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オレオレめもDockerでよく使うコマンド

*随時更新記事となります。すいません。

ルール

二回以上ググったら、ここに備忘録として残す

Docker イメージ

docker images -aq | xargs docker rmi #イメージ一括削除

Dockerのイメージの一括削除

docker-compose up -d --build

Dockerイメージをリビルドして、compose-up

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

12ステップで作る 組込みOS自作入門の開発環境をDockerで構築(gcc7系)

はじめに

 組み込み業務で使用しているクロスコンパイル環境(Ubuntu14.04)がサポート期間終了のためUbuntu18.04へ移行しました。その際にLinuxを実行する仮想環境もDockerに移行し、gccやbinutilsのバージョンアップを行いました。
 その際に得た知見を基に、組み込み学習教材として有名な12ステップで作る 組込みOS自作入門の開発環境をDocker上で構築し、さらにgccやbinutilsのバージョンも書籍で指定されているバージョン(gcc-3.4.6, binutils-2.19.1)からgcc-7.3.0, binutils-2.30に更新して動作確認を行いました。
 これから組込みOS自作入門を行う方や復習をする方のお役に立てればと思います。
 

動作確認ついて

既にgcc-3.4.6, binutils-2.19.1で開発していたkozosとbootloaderをgcc-7.3.0, binutils-2.30で再ビルドして基板に書き込みました。それからboot、kozosの起動、echoコマンドの確認を行いました。よって、各章で少しずつ開発されるbootloaderとkozosの全ての動作確認を行ったものではないことをご了承ください。

動作環境

  • ホストOS
    • Windows10
  • 仮想環境
    • Docker for windows (version 18.09.2, build 6247962)
  • ゲストOS
    • Ubuntu 18.04
  • h8300 Cross Compiler
    • gcc-7.3.0
    • binutils-2.30.0

環境構築

 Dockerfileをgit clone して、docker buildするだけです。作成したDockerfileはここにあります。
 以下にリポジトリからDockerfileをcloneしてdocker runするまでのコマンド例を示します。

>> git clone https://github.com/kjmatu/12step_self_embedded_os_dev_enviroment
>> cd 12step_self_embedded_os_dev_enviroment
>> docker build . -t h8300:ubuntu <- ファイルダウンロードやコンパイルで時間がかかります
>> docker run -it --rm --name kozos -v absolute_path_to_kozos_dir:/home/kozos h8300:ubuntu /bin/bash <- kozosを開発しているフォルダを/home/kozosにマウントして起動する
root@7c3849afbc62:/home#
root@7c3849afbc62:/home# h8300-elf-gcc -v
Using built-in specs.
COLLECT_GCC=h8300-elf-gcc
COLLECT_LTO_WRAPPER=/usr/local/libexec/gcc/h8300-elf/7.3.0/lto-wrapper
Target: h8300-elf
Configured with: ./../gcc-7.3.0/configure --disable-libstdcxx-pch --disable-libssp --disable-nls --disable-shared --disable-threads --enable-gold --enable-languages=c --enable-lto --enable-sjlj-exceptions --prefix=/usr/local --target=h8300-elf --with-gmp=/usr/local --with-mpfr=/usr/local --with-mpc=/usr/local --disable-bootstrap
Thread model: single
gcc version 7.3.0 (GCC)
root@7c3849afbc62:/home# h8300-elf-as -v
GNU assembler version 2.30 (h8300-elf) using BFD version (GNU Binutils) 2.30

bootloaderの書き込み

 ビルド環境は、Docker上で構築できたのですがDocker for windowsではシリアルポートの認識がDocker上でできないようなのでビルドしたbootloaderの書き込みはWindowsかWSL上のLinuxから行います。
 そのため書籍には書き込み時に行っているobjcopyをビルド時に行うように修正する必要があります。それ以外は書籍に指示された方法でビルドしたオブジェクトを書き込みます。
 結局は、ビルドをDocker上のUbuntuで行い、Bootの書き込みをWindowsかWSLで行うという統一性のない手続きになってしまっています。どなたか、Docker for windowsでデバイスを認識する方法をご存知ありませんか?

最後に

 注意として、この環境はあくまでgcc7系でH8300のクロスコンパイラが動き、kozosの動作が確認できたという実験的な環境です。書籍として学習する際に余計なエラーが出る可能性がありますのでまずは書籍に指示されている環境での学習をおすすめします。

参考

DockerでH8/3069Fビルド環境構築

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker Desktop の復習と、Windows Container に入門: Windows Server Container 理論編

前回は、Docker Desktop を Linux Container Mode で利用した際の構成についてまとめた。

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 Container についての概要と、Windows Server Container の理論についてまとめる。
また、実際にどの様に動作するのか、具体的にも迫れるだけ迫ってみたい。

前準備

門をくぐる前に、準備しなければいけないことがある。

Mode の切り替え

まずは、Linux Container Mode からの切り替える。

Docker Desktop の場合、タスクバーのコンテキストメニューから簡単に切り替えることができる。

念の為、一度 Docker Desktop の Restart をしておくと良い。

調査ツール導入

Windows の調査する場合、Sysinternals を入れておくと便利。
scoop で簡単に入るのでオススメ。

PS> scoop install sysinternals

● Process Explorer
全 Process についての基本的な情報を閲覧できる基本ツール。

● Process Monitor
Process による File 操作, Registry 操作, Network, Event などの全ログを確認できるツール。

● Object Manager namespace viewer
Windows Container の要である Object Namespace を閲覧するためのツール。

前準備終わり


Windows Container とは

そもそも Windows Container とは、Windows の NT Kernel で動作する Container のこと と、少なくとも自分は分類している。

Docker Desktop for Win の Linux Container Mode も、Linux Container on Windows も、名前に Windows と入っていても実際には Linux Kernel で動作している Linux Container だ。

Docker Engine

Docker Engine ( Daemon ) にも、Windows Platform 向けと Linux Platform 向けがある。
関係性は以下となっている。

Kernel Docker Engine Platform
Windows Server Container NT Kernel Windows
Windows Hyper-V Container NT Kernel Windows
LCOW Linux Kernel Windows
Linux Container Linux Container Linux

Document

Windows Container は兎に角ドキュメントが少なく、その少ない情報から推測していくしかない。
網羅的に書かれている資料としては、以下が参考になると思う。

コンテナの種類

Windows Container で扱う Container の種類を見ていく。

まず、Windows Container には、Process IsolationHyper-V Isolation という 2 つの分離レベルがある。

分離レベル : Process Isolation

Kernel を Host と共有し、Container は 1 つの Process として動作する方式。
Windows Server Container, Windows Process Container とも呼ばれる。

同じ方式を取っている Container Runtime としては runCrkt 等がある。

  • 利点
    • Process なので、Memory Footprint が小さい
    • Hyper-V が不要
  • 欠点
    • kernel を共有するため、悪意ある Container からの Kernel Exploit リスクがある

--isolation process と指定することで切り替えが可能

PS> docker run -it --isolation process mcr.microsoft.com/windows/nanoserver:1809 cmd.exe

分離レベル : Hyper-V Isolation

Container 毎に軽量 VM ( UtilityVM ) を立ち上げ、その上に Container を建てる方式。
Windows Hyper-V Container とも呼ばれる。
https://docs.microsoft.com/ja-jp/virtualization/windowscontainers/manage-containers/hyperv-container

同じ方式を取っている Container Runtime としては Kata Containers 等がある。

  • 利点
    • kernel レベルで分離されるので、Host への Kernel Exploit は原理上起こりえない
  • 欠点
    • Memory Footprint がでかい
    • Hyper-V が有効になっている必要がある

--isolation hyperv と指定することで切り替えが可能

PS> docker run -it --isolation hyperv mcr.microsoft.com/windows/nanoserver:1809 cmd.exe

番外 : LCOW ( Linux Containers on Windows )

これは Linux Container であるが、Windows Container Ecosystem では重要な役割を担う。

LCOW とは、Windows Docker Engine から Linux Container を立ち上げるという機能。
原理としては、Windows Hyper-V containers と同じで、軽量 VM ( LinuxKit for lcow ) を立ち上げて、その上で Linux Container を立ち上げる。
https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/linux-containers

Windows Hyper-V containers の原理を考えれば、当然これは出来るだろうと想像していたが、Windows Container 用に Container Image を作り直す必要がない、というのは思っていた以上に重要な機能だったとしみじみ感じている

  • 利点
    • Windows Container 用に Container Image 作り直す必要がなく、既存の資産が利用できる
  • 欠点
    • Linux Container Mode との Image Cache の共有ができない
      • まぁ、面倒と容量の問題

--platform linux と指定することで切り替えが可能

PS> docker run -it --platform -p 80:80 linux nginx

インターフェイス

次は、Docker Engine から Kernel まで間にある、様々な Interface を見ていく。

その前に復習として、Linux Docker Engine の場合は、以下のような構成になっている。

HCS ( Host Compute Service )

Windows Container を構成するいくつかの低レベル機能の操作を抽象化した Interface を提供するサービス。

https://docs.microsoft.com/en-US/virtualization/windowscontainers/deploy-containers/containerd
https://blogs.technet.microsoft.com/virtualization/2017/01/27/introducing-the-host-compute-service-hcs/

Windows Container を Docker Engine と統合する際、cgroups や Namespaces 等の Interface をそれぞれ模すような API を提供するという選択肢もあったが、安定性や応用性を考慮してかそれを選ばず、新たに安定的で使いやすい抽象 API を提供するサービスを構築することにした。

HCS は、Job Object や Silos、UnionFS 等の操作をする API を提供するサービス。
実態は vmcompute.dll である。

また、直接 C API を呼び出さなくても利用できるよう、Go Wrapper である hcsshimC# Wrapper がある。
現在、Windows Docker Engine には hcsshim が組み込まれている。

HCN ( Host Compute Network )

https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/architecture

HCN は、元々 HNS ( Host Network Service ) と呼ばれていた機能で、仮想 Switch や Firewall, Endpoint 設定等の提供するサービス。
実態は computenetwork.dll と思われる。

HCS と同様 hcsshim で呼び出しができる。
https://github.com/Microsoft/hcsshim/tree/master/hcn

OCI, CRI

Linux の構成と見比べれば一目瞭然だが、上記構成では Docker Engine ( の中の hcsshim ) から直接 HCS, HcN を呼び出している為、OCI ( Open Container Initiative ) や CRI ( Container Runtime Interface ) に対応できない。

そこで、runhcs という OCI に準拠した実装を用意している。

将来は、Windows Platform containerd + runhcs の構成になる模様。

明言されていないが、Linux 版 containerd は Networking 辺りも担当しているので、Windows 版 containerd が HCN に対応していくと予想。

プロセス分離原理

まずは、Windows process container がどの様に Host 環境から分離されているのかを見ていく。

構成技術の比較

技術的について網羅的にまとめられている以下を参考に見ていく。
Windows container security - Docker, Inc.

まずは、Linux Process Container との比較。

Windows container security - Docker, Inc.
Linux
Container
Windows
Process
Container
Resource Limitation
(CPU,Memory,IO)
cgroups Job Objects
Syscall Filtering seccomp Win32k Blacklist
Sandboxing Capability AppContainer
Change Root pivot_root Silos
Registry × Silos
UnionFS aufs, overlayfs, ... wcifs
Process Namespaces Silos
Network Network Namespaces Silos?

聞き慣れない要素が多いので、1 つずつ見ていく。

Job Objects

Windows container security - Docker, Inc.

Process を Group 化し管理できる機能。
https://docs.microsoft.com/en-us/windows/desktop/ProcThread/job-objects

デフォルトでは Child Process は同じ Job に属するので、Process Tree の一部 Branch をまとめて管理するという使い方ができる。

  • Process を Group 単位で操作
  • Group 毎の Resource Limitation
    • Execution time
    • CPU affinity
    • Memory Usage
    • Priority
    • Number of process
  • Job 内の Process 死亡を検知

Job object 機能自体は古くからあるものだが、Anniversary Update により、作成された Job が JID ( Job ID ) という識別子を持つように変更された。

Silo

Windows container security - Docker, Inc.

Job Objects を拡張し、リソース ( NT Object ) を Namespace 毎に分離する機能も持たせたもの。

元々 Windows は 1 つの KernelMode と 1 つの UserMode しか持っていなかったが、Windows Server 2016 以降は複数の UserMode を持てるようになった。Silos は Host の UserMode とは別に、Windows Container という特殊な UserMode を作っているという事らしい。

Silo は、Namespace 作成後、JID 経由で Job Object に assign される。
( 下図は WinObj のキャプチャ。 2136 3572 が JID )

Silo の操作は基本的には vmcompute.dll からしかできない様になっている。

  • Namespace による分離
    • NT Object
    • Registry
    • Network
    • volume mount

Document がない !!!

多分、分離機能の中で最も重要な要素であるはずなのに、とにかく情報がない。
探した中で最も詳しく書かれている文書は、分厚い Windows Internals だった ( 以下は Google Books の Preview )。
Windows Internals, Part 1: System architecture, processes, threads, memory management, and more - Google Books

その他、以下動画や資料などで情報を補完した。

Syscall Filtering

Windows container security - Docker, Inc.

Container 内からの Syscall を制限する機能。

全ての Syscall が対象ではなく、Win32k.sys のみらしい。
脆弱性が度々発覚し kernel exploit の Target となり易いからと考えられる。

詳細な原理についての説明は見つけられなかった。
見つけた中でこれらに一番近いものとして、Edge の Win32k Syscall Filtering がある。

Win32k Syscall Filtering

Win32k Syscall API は 約 1400 あるが、その中で Edge が動作するのに必要最低限の Syscall API のみアクセスを許可するフィルタ機能。
https://www.slideshare.net/PeterHlavaty/rainbow-over-the-windows-more-colors-than-you-could-expecthttps://blogs.technet.microsoft.com/iftekhar/2017/08/28/threat-mitigation-in-windows-10/
https://improsec.com/tech-blog/win32k-system-call-filtering-deep-dive

ただし、これは Edge にしか搭載していないとあり、Windows Container に使われているという情報は見つけられなかった。

Sandboxing ( Capability ACL )

Windows container security - Docker, Inc.

この文脈としての Sandboxing とは、Capability ACL について言及していると考えられる。
Linux の capability の場合、root の持つ権能を分割し、個別に Process に Add/Drop する事ができる。
アプリケーションの実行環境として Container を使う分には不要な機能だが、例えば Container を管理する Container を立ち上げる場合などでは必須な機能となる。

これも詳細な原理については説明は無かったが、AppContainer というキーワードがあり、これに関連すると考えられる。

AppContainer

アプリケーション単位で実行環境を Sandboxing する機能。主に Windows Store アプリを安全に動作させる事を目指し導入された。

AppContainer は Integrity : AppContainer という Low よりも強く操作が制限された状態で起動しながらも、マニフェストファイルで宣言した Capability がそれぞれ別途付与される。これにより、Capability の細やかな管理を可能としている。
screenshot_32.png

与えられた Capability は、専用の特殊な Group ( Flags: Capability, Name: APPLICATION_PACKAGE_AUTHORITY\XXXXXXX ( SID: S-1-15-3-XXXXX ) ) への所属をもって管理される。
image.png

wcifs ( Windows Container Isolation FileSystem )

Windows container security - Docker, Inc.

Windows で Union FS Like な Layered Filesystem を実現する FS Filter Driver。
https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/filter-manager-concepts

Windows へ向けた多くのアプリケーションは FileSystem が NTFS であることを期待し、新しい Filesystem を追加するのは困難であった。
その為、wcifs はベースは NTFS で、新たに Filter Driver のみ追加し、擬似的にそれを実現しているようだ。
Filter Driver の実体は、wcifs.sys

  • NTFS と認識
  • Layer Capabilities
    • 下位層は Reparse Points ( NTFS の SymLink ) により参照
    • 最上位層として、Virtual Hard Disk を利用
  • Copy on Write
    • 変更を Filter がキャッチし、最上位層である Virtual Hard Disk に書き込む

動作確認 : Windows process container

では、実際の環境を見ていく。

PS> $ docker info
# Containers: 0
#  Running: 0
#  Paused: 0
#  Stopped: 4
# Images: 4
# Server Version: 18.09.2
# Storage Driver: windowsfilter (windows) lcow (linux)
#  Windows:
#  LCOW:
# Logging Driver: json-file
# Plugins:
#  Volume: local
#  Network: ics l2bridge l2tunnel nat null overlay transparent
#  Log: awslogs etwlogs fluentd gelf json-file local logentries splunk syslog
# Swarm: inactive
# Default Isolation: hyperv
# Kernel Version: 10.0 17763 (17763.1.amd64fre.rs5_release.180914-1434)
# Operating System: Windows 10 Pro Version 1809 (OS Build 17763.316)
# OSType: windows
# Architecture: x86_64
# CPUs: 4
# Total Memory: 15.82GiB
# Name: SG04-NB-038
# ID: 
# Docker Root Dir: C:\ProgramData\Docker
# Debug Mode (client): false
# Debug Mode (server): true
#  File Descriptors: -1
#  Goroutines: 26
#  System Time: 2019-02-23T00:31:45.5057749+09:00
#  EventsListeners: 1
# Registry: https://index.docker.io/v1/
# Labels:
# Experimental: true
# Insecure Registries:
#  127.0.0.0/8
# Live Restore Enabled: false
# Product License: Community Engine

以下で Docker から Windows Container を立ち上げ、その様子を観察していく。

PS> docker run -it --isolation process mcr.microsoft.com/windows/nanoserver:1809 cmd.exe

Services

Windows は Linux とは違い、直接 Syscall せずに DLL を介して kernel Mode にアクセスする。
しかし、これら DLL が依存する System Service が User Mode に存在する為、Container はこれら System Service を丸ごと含む必要がある。

docker run ~ すると、smss.exe の下に新たに smss.exe ができ、また csrss.exewininit.exe Tree が新たに起動されるのが確認できる。
image.png
image.png
Windows Internals によると、Silo Namespace 作成後、

  1. Job Object に関連付けられた Smss を作成
  2. Smss が Session 0 の初期化処理として、Wininit.exe, Csrss.exe を起動
  3. Wininit.exeservices.exe, Lsass.exe を起動し、自動起動サービスが立ち上がっていく
  4. CExecSvc.exe サービスが、Docker run で指定されたコマンドを実行する

という順番で起動していくとのこと。

Linux Docker の docker run --init みたいなイメージかな。

Object

WinObj で Object の変化を見てみる。

Container Job Object

\ の中に Container_<<container_id>> という Job Object が追加されている。
これは、Process に関連付けられた Job Name と一致する。

PS> docker inspect 19a | wsl jq '.[0].Id'
# "19a639be74b7a2569e37d26fef02637039ddce3c3408b61c23f9a7d1f1f6bee1"

screenshot_84.png
screenshot_85.png

Silo Namespace

\Silos\ という Directory 以下に 4 桁数字の Directory が追加される。
この数字が JID になるようだ。

  • \Device\
    • \Global\Device への SymLink
      • アクセスできる Device が絞られている
    • NamedPipe, MountPointmanager, Mailslot, MQAC だけ SymLink ではなく、Namespace 内に分離されている
  • \GLOBAL??\
    • ここにある Object は Userspace からアクセスできる
      • \\. で呼び出す ( ex. \\.\pipe\\GLOBAL??\pipe )
  • \DosDevice
    • MS-DOS Device Object
    • Global??\ への SymLink
    • COM Port や Drive を示す際に使われる Alias ?
  • \Driver, Filesystem, Registry
    • Global??\ への SymLink
    • Host と同じものを利用
  • \GLOBAL??\C:
    • Drive Letter Object
    • Volume に Drive Letter を割り当てるとできる
    • HarddiskVolume への SymLink
      • \DosDevice\C:\GLOBAL??\C:\Device\HarddiskVolume4
  • \GLOBAL??\Volume{<GUID>}
    • Volume Object
    • HarddiskVolume への SymLink
      • \DosDevice\Volume{...}\GLOBAL??\Volume{...}\Device\HarddiskVolume4
  • \SystemRoot
    • Windows System の Root Object
    • Global??\C:\windows への SymLink

● Kernel Object

Kernel レベルで見てみる。
今回は LiveKD を使って以下で WinDbg を起動する。

PS> livekd.exe -k "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe"

Silo 一覧は、!Silo で見られる。
screenshot_16.png
Silo の情報はこんな感じ。
screenshot_19.png
ちなみに、Host のは !Silo -g Host で見られる。
screenshot_18.png
Globals はこんな感じ。
screenshot_21.png

Container Template File

Windows Container を作る時、wsc.def ( Windows Server Container ? ) という定義を元に作成される。
%SystemRoot%\System32\Containers\wsc.def にある。
あくまで Template なので、Container の立ち上げ方次第では設定値は変わるはず。

Object Section

wsc.def
<container>
    <namespace>
        <ob shadow="false">
            <symlink name="FileSystem" path="\FileSystem" scope="Global" />
            <symlink name="PdcPort" path="\PdcPort" scope="Global" />
            <symlink name="SeRmCommandPort" path="\SeRmCommandPort" scope="Global" />
            <symlink name="Registry" path="\Registry" scope="Global" />
            <symlink name="Driver" path="\Driver" scope="Global" />
            <objdir name="BaseNamedObjects" clonesd="\BaseNamedObjects" shadow="false"/>
            <objdir name="GLOBAL??" clonesd="\GLOBAL??" shadow="false">
                <!-- Valid links to \Device -->
                <symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Local" />
                <symlink name="UNC" path="\Device\Mup" scope="Local" />
                <symlink name="Tcp" path="\Device\Tcp" scope="Local" />
                <symlink name="MountPointManager" path="\Device\MountPointManager" scope="Local" />
                <symlink name="Nsi" path="\Device\Nsi" scope="Local" />
                <symlink name="fsWrap" path="\Device\FsWrap" scope="Local" />
                <symlink name="NDIS" path="\Device\Ndis" scope="Local" />
                <symlink name="TermInptCDO" path="\Device\TermInptCDO" scope="Local" />
            </objdir>
            <objdir name="Device" clonesd="\Device" shadow="false">
                <symlink name="Afd" path="\Device\Afd" scope="Global" />
                <symlink name="ahcache" path="\Device\ahcache" scope="Global" />
                <symlink name="CNG" path="\Device\CNG" scope="Global" />
                <symlink name="ConDrv" path="\Device\ConDrv" scope="Global" />
                <symlink name="DeviceApi" path="\Device\DeviceApi" scope="Global" />
                <symlink name="DfsClient" path="\Device\DfsClient" scope="Global" />
                <symlink name="DxgKrnl" path="\Device\DxgKrnl" scope="Global" />
                <symlink name="FsWrap" path="\Device\FsWrap" scope="Global" />
                <symlink name="Ip" path="\Device\Ip" scope="Global" />
                <symlink name="Ip6" path="\Device\Ip6" scope="Global" />
                <symlink name="KsecDD" path="\Device\KsecDD" scope="Global" />
                <symlink name="LanmanDatagramReceiver" path="\Device\LanmanDatagramReceiver" scope="Global" />
                <symlink name="LanmanRedirector" path="\Device\LanmanRedirector" scope="Global" />
                <symlink name="MailslotRedirector" path="\Device\MailslotRedirector" scope="Global" />
                <symlink name="Mup" path="\Device\Mup" scope="Global" />
                <symlink name="Ndis" path="\Device\Ndis" scope="Global" />
                <symlink name="Nsi" path="\Device\Nsi" scope="Global" />
                <symlink name="Null" path="\Device\Null" scope="Global" />
                <symlink name="PcwDrv" path="\Device\PcwDrv" scope="Global" />
                <symlink name="RawIp" path="\Device\RawIp" scope="Global" />
                <symlink name="RawIp6" path="\Device\RawIp6" scope="Global" />
                <symlink name="Tcp" path="\Device\Tcp" scope="Global" />
                <symlink name="Tcp6" path="\Device\Tcp6" scope="Global" />
                <symlink name="Tdx" path="\Device\Tdx" scope="Global" />
                <symlink name="Udp" path="\Device\Udp" scope="Global" />
                <symlink name="Udp6" path="\Device\Udp6" scope="Global" />
                <symlink name="VolumesSafeForWriteAccess" path="\Device\VolumesSafeForWriteAccess" scope="Global" />
                <symlink name="VRegDriver" path="\Device\VRegDriver" scope="Global" />
                <symlink name="WMIDataDevice" path="\Device\WMIDataDevice" scope="Global" />
                <symlink name="TermInptCDO" path="\Device\TermInptCDO" scope="Global" />
                <symlink name="RdpVideoMiniport0" path="\Device\RdpVideoMiniport0" scope="Global" />
            </objdir>
            <objdir name="NLS" clonesd="\NLS" shadow="false"/>
            <objdir name="UMDFCommunicationPorts" clonesd="\UMDFCommunicationPorts" shadow="false"/>
        </ob>
...

\FileSystem, \PdcPort, \SeRmCommandPort, \Registry, \Driver\Global\* に直接 Link されているようだ。
\BaseNamedObjects\GLOBAL?? は Clone されているので別物。

Job Section

wsc.def
<container>
    <namespace>
        ....
        <job>
            <systemroot path="C:\Windows" />
        </job>
        ....

Job に SystemRoot を設定する必要性が不明。

Mountmgr Section

wsc.def
<container>
    <namespace>
        ....
        <mountmgr>
        </mountmgr>
        ....

Default では特に何もしない。

NamedPipe Section

wsc.def
<container>
    <namespace>
        ....
        <namedpipe>
        </namedpipe>
        ....

これも Default では特に何もしない。

Registry Section

wsc.def
<container>
    <namespace>
        ....
        <registry>
            <symlink
                key="$SiloHivesRoot$\Silo_$SiloName$_Security\SAM"
                target="\Registry\Machine\SAM\SAM"
                />
            <symlink
                key="$SiloHivesRoot$\Silo_$SiloName$_User\S-1-5-18"
                target="\Registry\User\.Default"
                />
            <symlink
                key="$SiloHivesRoot$\Silo_$SiloName$_System\CurrentControlSet"
                target="\Registry\Machine\SYSTEM\ControlSet001"
                />
            <symlink
                key="$SiloHivesRoot$\Silo_$SiloName$_System\ControlSet001\Hardware Profiles\Current"
                target="\Registry\Machine\System\ControlSet001\Hardware Profiles\0001"
                />
            <hivestack hive="machine">
            </hivestack>
            <hivestack hive="security">
            </hivestack>
            <hivestack hive="system">
            </hivestack>
            <hivestack hive="software">
            </hivestack>
            <hivestack hive="sam">
            </hivestack>
            <hivestack hive="user">
            </hivestack>
            <hivestack hive="defaultuser">
            </hivestack>
            <RedirectionNode
                ContainerPath="\Registry\MACHINE"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_Machine"
                access_mask="0xffffffff"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\Hardware"
                HostPath="\Registry\MACHINE\Hardware"
                access_mask="0x83020019"
                TrustedHive="true"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\SOFTWARE"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_Software"
                access_mask="0xffffffff"
                TrustedHive="true"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\SYSTEM"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_System"
                access_mask="0xffffffff"
                TrustedHive="true"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\Nsi"
                HostPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\Nsi"
                access_mask="0x83020019"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\SystemInformation"
                HostPath="\Registry\MACHINE\SYSTEM\ControlSet001\Control\SystemInformation"
                access_mask="0x83020019"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\SAM"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_Sam"
                access_mask="0xffffffff"
                TrustedHive="true"
                />
            <RedirectionNode
                ContainerPath="\Registry\MACHINE\Security"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_Security"
                access_mask="0xffffffff"
                TrustedHive="true"
                />
            <RedirectionNode
                ContainerPath="\Registry\USER"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_User"
                access_mask="0xffffffff"
                />
            <RedirectionNode
                ContainerPath="\Registry\USER\.DEFAULT"
                HostPath="$SiloHivesRoot$\Silo_$SiloName$_DefaultUser"
                access_mask="0xffffffff"
                />
        </registry>
        ....

Registry は後ほど詳細を見ていく。

Resource Limitation

Job Object の持つ Resource 制限機能を利用している。

試しに Memory Limitation をかけてみる。
https://docs.microsoft.com/en-us/windows/desktop/ProcThread/job-objects#job-limits-and-notifications

PS> docker run -it --isolation process -m "100m" mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
PS> docker inspect dd | wsl jq '.[0].HostConfig.Memory'
# 104857600

この Container の Job Object を ProcessExplorer で見てみると、Memory Limit がかかっているのが確認できる。
screenshot_53.png

Process

● Container 内

Container 内から見えるのは、Job Object に関連付けられた smss が立ち上げた子プロセスに限られる。
screenshot_14.png

(CONTAINER)> tasklist /SVC
# Image Name                     PID Services
# ========================= ======== ============================================
# System Idle Process              0 N/A
# System                           4 N/A
# smss.exe                     11804 N/A
# csrss.exe                    18240 N/A
# wininit.exe                  17392 N/A
# services.exe                 12668 N/A
# lsass.exe                     8328 SamSs
# svchost.exe                  11272 DcomLaunch, LSM, SystemEventsBroker
# svchost.exe                  16444 RpcEptMapper, RpcSs
# fontdrvhost.exe              14512 N/A
# svchost.exe                  12728 gpsvc, iphlpsvc, ProfSvc, Schedule, SENS,
#                                    UserManager, UsoSvc, Winmgmt
# svchost.exe                  14768 EventSystem, nsi
# CExecSvc.exe                 17812 cexecsvc
# svchost.exe                  15756 Dhcp, EventLog, TimeBrokerSvc,
#                                    WinHttpAutoProxySvc
# svchost.exe                   6204 CryptSvc, Dnscache, LanmanWorkstation, WinR
# conhost.exe                  16212 N/A
# powershell.exe               16728 N/A
# svchost.exe                  15860 CoreMessagingRegistrar
# svchost.exe                  12604 DiagTrack
# svchost.exe                  18144 SysMain
# msdtc.exe                    11284 MSDTC
# tasklist.exe                 10076 N/A
# WmiPrvSE.exe                 18920 N/A

唯一、smss 自身と System, System Idle Process が見えている。Kernel Mode を共有しているので当然か。
screenshot_15.png
不思議なのが、Container の smss を立ち上げているのが、PID 4 の方の smss で、User Mode の下に別の User Mode がぶら下がっているんだなぁと。
以下のようにはならないんだ。

System
├─ smss ( PID : 4 )
└─ smss ( PID : 11804 )

● Container 外

さて、外からどう見えるかと言うと、Process Explorer で見えている事からも分かる通り、丸見えとなる。
これは、Linux Process Container と同じ挙動だ。

PS> tasklist /SVC | findstr 16728
# powershell.exe               16728 N/A

● 分離原理

正直、これは分からない。
資料などを見ていると、Process Table を Silo が分離しているという説明が多い。

● Kernel Object

Kernel レベルで見てみる。

ServerCore Container を 1 つ立ち上げ、起動した powershell.exe を調べる。
powershell.exe に Job が関連付けられているのが分かる。
screenshot_06.png
Job を調べると、Silo Flag が付けられている。
screenshot_07.png
その Silo は Server Silo Type で、RootDirectry が \Silos\1192 と分かる。
screenshot_10.png

◆ 結論

答えはわからないが、 Process 単位では Job ( Server Silo ) との関連を持っているので、その辺でゴニョゴニョしてるのかなぁ。

Syscall Filtering

足がかりが全く無い。関係ありそうな情報としては、以下などが見つかったが。
https://improsec.com/tech-blog/win32k-system-call-filtering-deep-dive
http://redplait.blogspot.com/2016/11/w32pservicetablefilter-from-windows-10.html

参考サイト の通りにやってみたが、MicrosoftEdgeCP.exe の _EPROCESS は EnableFilteredWin32kAPIs Field を持っていなかった。
screenshot_29.png

W32pServiceTableFilterW32pArgumentTableFilter 辺りなのかなぁとは思いつつ、これ以上 Deep な世界に行く能力もなく、ここらへんで断念。

◆ 結論

詳細な動作原理が分からなかった。

そもそも、Docker の --security-opt に Windows で使えそうなセキュリティオプションがなく、カスタマイズのしようが無い。
https://docs.docker.com/engine/reference/run/#security-configuration

Sandboxing for capability

● AppContainer

まずは、AppContainer についてざっと見てみる。

Store App である Calculator の場合。
インターネット接続 ( S-1-15-3-1 ) Capability が付与されていると分かる。
image.png
Calculator マニフェストファイル を確認すると、確かに Capability が付与されている。

<?xml version="1.0" encoding="utf-8"?> 
<Package xmlns="http://schemas.microsoft.com/appx/2010/manifest" xmlns:build="http://schemas.microsoft.com/developer/appx/2012/build" IgnorableNamespaces="build"> 
  ...
  <Capabilities> 
    <Capability Name="internetClient" /> 
  </Capabilities> 
  ...
</Package>

設定できる Capabilities は、以下に一覧されている。
https://docs.microsoft.com/ja-jp/windows/uwp/packaging/app-capability-declarations

確かにこれを応用すれば、Linux の Capability と同じ様な機能を実現できそうだ。

● Windows Container

次は Windows Container を立ち上げて見てみる。

意外にも、Container 内の powershell.exe の Integrity は High で、UAC 昇格後と同等の相当高いレベル。
screenshot_35.png
Group にも、Capability に関するものが何もない。
screenshot_36.png
もしかして、全く違う機構で動いているんだろうか。

● Docker Option

よく分からないので、--cap-add=SYS_ADMIN オプションを付けて起動してみるが、Integrity も Group も変化が無かった。

PS> docker run -d --isolation=process --cap-add=SYS_ADMIN  mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
PS> docker inspect 633  | wsl jq '.[0].HostConfig.CapAdd'
# [
#   "SYS_ADMIN"
# ]

じゃあ、今度は思い付きで --cap-drop=internetClient とやってみる。

PS> docker run -it --isolation=process --cap-drop=internetClient  mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
PS> docker inspect 633  | wsl jq '.[0].HostConfig.CapDrop'
# [
#   "internetClient"
# ]

(CONTAINER)> curl https://httpbin.org/get
# {
#   "args": {},
#   "headers": {
#     "Accept": "*/*",
#     "Host": "httpbin.org",
#     "User-Agent": "curl/7.55.1"
#   },
# ...

多分間違ってるのに起動はしてしまうんだ。当然効果はない。

そもそも、Docker 公式ドキュメントの --cap-add --cap-drop には Add/Drop Linux capabilities と記述があり 、付けても意味がないのかも。

◆ 結論

分からなかった。
そもそも、Windows Container の Capability について語られているものがこの資料しか見つからない。

動作原理はおろか、本当に動いているのかどうかすら分からなかった。Windows はお呼びでない可能性もある。
発表資料にある Capability-based Access Control とは、一体何だったのだろう。

Change Root

振る舞いを明確に解説した情報が見つけられなかったので、Object の設定から調べた。
新たに JID 2136 の Container を立ち上げたとする。

\Silos\2136\GLOBAL??\C:\Silos\2136\GLOBAL??\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b} の Link は HarddiskVolume68 を指している。
screenshot_59.png
screenshot_65.png
HarddiskVolume68 は、Disk 2 の Partition 2 上に mount されている。( Volume と Disk の関連を確実に見つけるなら、この方法で で )
image.png
これを diskpart で確認すると、Disk 2 は C:\ProgramData\Docker\windowsfilter\d67daa4ef88...\sandbox.vhdx にある Virtual Hard Disk であると分る。

PS> diskpart
DISKPART> list vdisk
#     仮想ディスク ###  ディスク ###  状態                  種類        ファイル
#   ----------------  ------------  --------------------  ----------  --------
#   仮想ディスク 0    ディスク 2    アタッチされたディスクは開いていません   拡張可能        C:\ProgramData\Docker\windowsfilter\d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286\sandbox.vhdx
#   仮想ディスク 1    ディスク 1    アタッチされたディスクは開いていません   拡張可能        C:\ProgramData\Docker\windowsfilter\ff205a90d5d80582a0a73df0b388ea4fb63367d2155e3102325b194d9b124acb\sandbox.vhdx

Disk 2 の実体のある d67daa4ef88.... フォルダは、先程立ち上げた Container の ID と一致する。

つまり、Container が立ち上がると、新たに windowsfilter フォルダ下に Container ID フォルダが作成され、そこに新たな Virtual Hard Disk が作られる。
Container の Silo Namespace にある ~\GLOBAL??\C:~\GLOBAL??\Volume{...} の参照 先を、先程作成された Virtual Hard Disk に向けることで、pivot_root ( chroot ) と同等な File System Sandbox 機能を実現していると考えられる。
docker-Page-13.png

◆ 結論

これら情報を照らし合わせると、

  1. Container 起動
  2. C:\ProgramData\Docker\windowsfilter 以下に Container ID と同名のフォルダ作成
  3. その中に Virtual Hard Disk ( 差分 Disk ( 詳細後述 ) ) を作成、Volume として利用可能な状態とする
  4. Silo Namespace 内の \Global??\C:\Global??\Volume{...} の参照先を 3 の Volume に
  5. Container の Boot 時には、\SystemRoot の参照先である \Global??\C:\windows から Windows を立ち上げる

と予測できる。

:alembic: 実験 :microscope:

試しに、Container を一旦止めて、Host から閲覧可能かやってみる。
Container を止めると Virtual Hard Disk は止まってしまうので、attach から始める。

PS> diskpart
DISKPART > list vdisk
#   仮想ディスク ###  ディスク ###  状態                  種類        ファイル
#   ----------------  ------------  --------------------  ----------  --------
#   仮想ディスク 0    ディスク ---  追加済み                  不明          C:\ProgramData\Docker\windowsfilter\d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286\sandbox.vhdx

DISKPART > select vdisk file="C:\ProgramData\Docker\windowsfilter\d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286\sandbox.vhdx"
DISKPART > attach vdisk
#   100% 完了しました
# DiskPart により、仮想ディスク ファイルがアタッチされました。
DISKPART > list vol
#   Volume ###  Ltr Label        Fs    Type        Size     Status     Info
#   ----------  --- -----------  ----  ----------  -------  ---------  --------
# ...
#   Volume 6                      NTFS   Partition     19 GB  正常

DISKPART > select volume 6
# ボリューム 6 が選択されました。
DISKPART > assign letter=x
# DiskPart はドライブ文字またはマウント ポイントを正常に割り当てました。

screenshot_72.png
普通に中が見れた。ちなみに NTFS として認識される。

UnionFS

Virtual Hard Disk レベルで切り替えがされていることは分かった。
しかし、Docker の特徴でもある、差分毎の Image Layer や Copy on Write 等については、どの様に実現するのだろうか。
docker-Page-12 (1).png

通常、Linux Container の場合、overlayfs や aufs の様な Union filesystem を利用する。
https://docs.docker.com/storage/storagedriver/overlayfs-driver/
しかし、NTFS は union filesystem には対応できない。

Windows Container の Storage Driver は、lcow (linux) windowsfilter (windows) になっている。
どうやらこの windowsfilter が wcifs の事のようだ。

● Image Layer

Docker Image を pull すると、C:\ProgramData\Docker\windowsfilter 以下に 2 つのフォルダができる。

PS> docker pull mcr.microsoft.com/windows/nanoserver:1809
# 1809: Pulling from windows/nanoserver
# ...

PS> ls C:\ProgramData\Docker\windowsfilter | ft -Property Name,Attributes -HideTableHeaders
# 0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196  Directory
# e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038  Directory

これらは、展開された Image の各 Layer のファイル差分 ( Snapshot ? ) となっている。
例えば nanoserver:1809 は、2 つの Layer で構成されているのが分かる。

PS> docker image inspect mcr.microsoft.com/windows/nanoserver:1809 | wsl jq '.[0].GraphDriver'
# {
#   "Data": {
#     "dir": "C:\\ProgramData\\Docker\\windowsfilter\\0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196"
#   },
#   "Name": "windowsfilter"
# }

PS> cat C:\ProgramData\Docker\windowsfilter\0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196\layerchain.json
# ["C:\\ProgramData\\Docker\\windowsfilter\\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038"]

PS> cat C:\ProgramData\Docker\windowsfilter\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038\layerchain.json
# null

docker-Page-15.png

:alembic: 実験 :microscope:

C:\ProgramData\Docker\windowsfilter\<<Image Layer ID>>\Files が File 実体のはず。
という事で、Host の Explorer から直接ここを変更した場合、Container にどういった影響があるのか実験してみる。

・Layer 2 ( 上位 Layer ) を変更
C:\ 直下への変更はなぜか伝わらなかった。
加えた変更が伝わるフォルダと、伝わらないフォルダがある。
伝わらないフォルダは空フォルダであった。

・Layer 1 ( 下位 Layer ) に変更
やはり C:\ 直下への変更は伝わらない。
Layer 2 で伝わらなかった空フォルダへの変更のみ伝わった。

これら結果から、以下が予想される。

  • C:\ だけは特殊なフォルダ
  • フォルダ単位で参照しに行く Layer を記憶している ?
    • もしくは、上位が空なら下位に聞く、という事か ?
  • 下位 Layer に参照しに行くフォルダは、上位 Layer では空になっている
    • という事は、上位 Layer で空フォルダが無くなる = フォルダ削除 ?

● Merged Layer

まずは Container を立ち上げる。
取り敢えず、mount 状況を確認する。

PS> docker run -it mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
(CONTAINER)> mountvol C: /L
#     \\?\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}\

windowsfilter フォルダに、新たに Merged Layer を格納するフォルダが追加されている。

OtherTerminal
PS> docker ps
# CONTAINER ID        IMAGE                                       COMMAND                  CREATED             STATUS              # PORTS                NAMES
# 0934e4f05940        mcr.microsoft.com/windows/nanoserver:1809   "cmd.exe"                32 seconds ago      Up 28 seconds                            brave_jackson

PS> ls C:\ProgramData\Docker\windowsfilter | ft -Property Name,Attributes -HideTableHeaders
# 0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519  Directory
# 0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196  Directory
# e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038  Directory

PS> cat C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519\layerchain.json | wsl jq '.'
# [
#   "C:\\ProgramData\\Docker\\windowsfilter\\0b497555b76d5a782291d5e87de17720836130c7e1db0f3ac1519161a61c5196",
#   "C:\\ProgramData\\Docker\\windowsfilter\\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038"
# ]

Merged Layer の中身は、Storage の項で見たとおり Virtual Hard Disk になる。

PS> ls C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519 | ft -Property Name,Attributes -HideTableHeaders
# layerchain.json    Archive
# sandbox.vhdx       Archive

docker-Page-15.png

Container の中で、ファイルを編集してみる。
編集された影響は、C:\ProgramData\Docker\windowsfilter\<<Image Layer ID>>\Files へは及んでいない。
Copy on Write が実現できている様だ。

(CONTAINER)> dir
# 09/15/2018  04:14 PM             5,510 License.txt
# 02/26/2019  04:35 AM    <DIR>          Users
# 02/26/2019  04:35 AM    <DIR>          Windows

(CONTAINER)> echo 'hoge' > test.txt

(CONTAINER)> dir
# 02/26/2019  04:38 AM                 8 hoge.txt
# 09/15/2018  04:14 PM             5,510 License.txt
# 02/26/2019  04:35 AM    <DIR>          Users
# 02/26/2019  04:35 AM    <DIR>          Windows

:alembic: 実験 :microscope:

まずは、この sandbox.vhdx がどこから来たのか調べてみると、どうやら 差分 Disk だったらしい。
親フォルダの e0a98d172d8... は、Base とした Image の最下層 Layer を展開したフォルダであった。

PS> diskpart
DISKPART >  select vdisk file="C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519\sandbox.vhdx"
# DiskPart により、仮想ディスク ファイルが選択されました。

DISKPART > detail vdisk
# デバイスの種類 ID: 3 (不明)
# ベンダー ID: {EC984AEC-A0F9-47E9-901F-71415A66345B} (Microsoft Corporation)
# 状態: 追加済み
# 仮想サイズ:   20 GB
# 物理サイズ:   23 MB
# ファイル名: C:\ProgramData\Docker\windowsfilter\0934e4f05940c5201439620b49e7d6dca3895bb0f67334006bd8dd43e1050519\sandbox.vhdx
# 子: はい
# 親ファイル名: C:\ProgramData\Docker\windowsfilter\e0a98d172d86094ef950f7c7e270306e2998f2b1aa2a1874a5eda714ba2a8038\blank-base.vhdx
# 関連付けられたディスク番号: 見つかりません。

また、Copy on Write がどう実現されているのかを確認しよう。
Container 内部で write.log というファイルを作成すると、\Device\HarddiskVolume12\write.log に書き込まれているのが確認できた。
screenshot_92.png
どうやら Write は通常取り書き込まれているようで、つまり Read の時にその参照先を切り替える事で UnionFS を実現しているようだ。

● Data Volume Layer, Bind Mount

Data Volume は C:\ProgramData\Docker\volumes 内に設置される。

PS> docker volume create cache
# cache

PS> docker volume inspect cache | wsl jq '.[0].Mountpoint'
# "C:\\ProgramData\\Docker\\volumes\\cache\\_data"

PS> ls C:\ProgramData\Docker\volumes | ft -Property Name,Attributes -HideTableHeaders
# cache        Directory
# metadata.db  Archive

PS> docker run -it -v "cache:c:\temp" mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
(CONTAINER)> dir
# 09/15/2018  04:14 PM             5,510 License.txt
# 02/26/2019  05:57 AM    <DIR>          temp
# 02/26/2019  05:57 AM    <DIR>          Users
# 02/26/2019  05:57 AM    <DIR>          Windows
PS> docker inspect be13f4b02824 | wsl jq '.[0].Mounts'
# [
#   {
#     "Type": "volume",
#     "Name": "cache",
#     "Source": "C:\\ProgramData\\Docker\\volumes\\cache\\_data",
#     "Destination": "c:\\temp",
#     "Driver": "local",
#     "Mode": "",
#     "RW": true,
#     "Propagation": ""
#   }
# ]

Bind Mount も同様。

PS> docker run -it -v "c:\src:c:\src" mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
(CONTAINER)> dir
# 09/15/2018  04:14 PM             5,510 License.txt
# 02/26/2019  05:57 AM    <DIR>          temp
# 02/26/2019  05:57 AM    <DIR>          Users
# 02/26/2019  05:57 AM    <DIR>          Windows
PS> docker inspect be13f4b02824 | wsl jq '.[0].Mounts'
# [
#   {
#     "Type": "bind",
#     "Source": "c:\\src",
#     "Destination": "c:\\src",
#     "Mode": "",
#     "RW": true,
#     "Propagation": ""
#   }
# ]

docker-Page-16.png

● wcifs

File Read の参照先を切り替えているのが wcifs.sys だ。
https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/anti-virus-optimization-for-windows-containers

上記リンクより、Redirect には reparse points ( NTFS における Simlink ) を利用していると。
書き込みが起こった際には、書き込みは Sanbox 内に行い、reparse points をそっちに向けることで Copy on Write を実現しているらしい。
上記を透過的に行っているのが wcifs.sys になる。

まず、wcifs の設定を確認するため、HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\wcifs を見てみると、FltMgr の管理する Filter であることが分かる。
screenshot_86.png

また、Instance は Host に 1 つと、Container 毎に 1 つづつ存在している。
以下は、Container を 2 つ立ち上げている状態。wcifs Outer Instance は利用していない模様。

PS> fltmc instances -f wcifs
# wcifs フィルターのインスタンス:
# ボリューム名                           階層          インスタンス名       フレーム  Vl 状態
# -------------------------------------  ------------  ----------------------  -----  --------
# C:                                        189900     wcifs Instance            0
#                                           189900     wcifs Instance            0
#                                           189900     wcifs Instance            0


あとは、overlayfs の様に どの層をどの順番で重ねるか を指定する必要があるはずだが、

# overlayfs は、こんな感じに mount 時に指定する
$ mount -t overlay overlay -o lowerdir=/volumes/layer_0:/volumes/layer_1,upperdir=/volumes/layer_3,workdir=/volumes/layer_merged

それがどこなのかは分からなかった。これ以上は Kernel Debug でもしないと分からなそう。

◆ 結論

という事で、これら情報を照らし合わせると、

  1. Docker Image を Pull
  2. C:\ProgramData\Docker\windowsfilter 以下に Layer 毎にフォルダを作成
  3. C:\ProgramData\Docker\windowsfilter\\<<>GUID>\\Files に Layer のファイル展開
  4. docker volume create <<dir_name>> した場合は、C:\ProgramData\Docker\volumes 以下に <dir_name> フォルダを作成
  5. Container 起動
  6. Change Root の手順
  7. 何らかの方法 で、Layer 情報を wcifs に伝える or wcifs が参照するデータ ? ファイル ? Object ? に設定する
  8. Container 内で File Write が発生すると、sandbox.vhdk 上に変更が書き込まれ reparse points 先を変更
  9. Container 内で File Read が発生すると、reparse points 先を読み込む

と予測できる。

Registry

Registry も Windows においては重要なデータである。
App-V 1703 Virtual Registry and Containers?

● Registry とは

そもそも Registry は、Hive というファイルを HKLM, HKCUHKLM\SECURITY 等に mount して 1 つの大きな Tree Structure を構築したもの。
以下表を見ると分かる通り、mount, SymLink, 疑似 FS と NTFS よりも Linux のそれに近い。

実体 参照
HKEY_CLASSES_ROOT\ HKLM\SOFTWARE\ClassesHKCU\Software\Classes を merge した仮想 Key
HKEY_CURRENT_CONFIG\ HKLM\SYSTEM\CurrentControlSet\Hardware Profiles\Current
HKEY_USERS\<SID>\ C:\Users\<USERNAME>\NTUSER.DAT
HKEY_CURRENT_USER\ ログイン中の HKEY_USERS\<SID>\
HKEY_LOCAL_MACHINE\* C:\Windows\System32\config\*
HKEY_LOCAL_MACHINE\HARDWARE Hardware 情報を Registry として閲覧できる。
/proc みたいなもの。

つまり、Container は HKEY_LOCAL_MACHINEHKEY_USERS を管理すれば良いという事になる。

● 実践

という事で、まずは Process Explorer で確認してみたが、普通にアクセスしているようにしか見えない。
screenshot_75.png
どういう事 ?

● Hive ファイル

よく分からないので、新たに Container を立ち上げてその過程を Process Monitor で確認すると、\Registry\WC\Silo ~ というレコードが沢山出てきた ( 通常は、HKCU\…HKLM\… になる )。
screenshot_78.png
このレコードの最初の出処まで遡って見てみると、\Registry\WC\Silo9dd9eeab-...-b0acd579bc17system 等が RegLoadKey されていた。
image.png

その備考欄には Hive Path: Volume{9dd9eea3-...}\WcSandboxState\Hives\software_Delta とある。
9dd9eea3-... という GUID は立ち上げた Container の sandbox.vhdx の事で、これはつまり Container 内の Hive File を読み込んだんだ と分かる。

しかし、一体これらはどこで定義されているのか。
Windows は起動時に読み込まれる Hive の List を HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\hivelist に定義しているので、そこを確認してみると 、偶然 \REGISTRY\WC\Silo ~ という Value をいくつも見つけた。
screenshot_79.png

\REGISTRY\WC\Silo + <Random GUID> + [ machine | user ]
Value : \Device\HarddiskVolume4\Windows\System32\containers\machine_user

HarddiskVolume4 は Host の C: Volume の事。
つまり Host の C:\Windows\System32\containers\machine_user の事。

\REGISTRY\WC\Silo + <Random GUID> + [ defaultuser | sam | security | software | system ]
Value : \Device\HarddiskVolume76\WcSandboxState\Hives\ + [ defaultuser | sam | security | software | system ] + _Delta

Container の Virtual Hard Disk 上にある Hive File を指している。

\REGISTRY\WC\Silo + <Another Random GUID> + [ defaultuser | sam | security | software | system ]
Value : \Device\HarddiskVolume4\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\ + [ defaultuser | sam | security | software | system ] + _Base

0b497555b76d5... は Container の Base Image の最上位 Layer を展開したフォルダ名。
また、同じフォルダには XXX_Delta も存在している。
screenshot_97.png

:alembic: 実験 :microscope:

Hive ファイルが増えてややこしくなてきたので、少しまとめたい。
その為に、試しに各所の Software の Hive 復元してみよう。
Hive の復元は、regedit の Import 機能で適当な場所に展開する。

Container から Hive ファイルを抜き出す為に、Virtual Hard Disk に Drive Letter ( 今回は Q ) を与えて、Explorer 経由で取ってくる。

Container ID b6478d22cc7443...
Base Image Top Layer 0b497555b76d5...
Base Image Bottom Layer e0a98d172d860...

・Base Hive in Top Image
場所 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\Software_Base

・Delta Hive in Top Image
場所 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\Software_Delta

・Base Hive in Bottom Image
場所 : C:\ProgramData\Docker\windowsfilter\e0a98d172d860...\Hives\Software_Base

上記 3 つをそれぞれ復元し、比較すると、Base Hive in Top Image = Base Hive in Bottom Image + Delta Hive in Top Image と分かった。

screenshot_94.png
+
screenshot_95.png
=
screenshot_99.png

・System Hive in Container
場所 : Q:\Windows\System32\config\SOFTWARE
参照元 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Files\Windows\System32\config\SOFTWARE

Windows は起動時に smssC:\Windows\System32\config\ 以下の Hive を読み込んで Registry を構成する。

サイズを見て『もしや…』と思い C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Hives\Software_Base と比較すると一致した。どうやら同じもののようだ。

PS> reg compare "HKEY_CURRENT_USER\RestoreFromContainer\SOFTWARE" "HKEY_CURRENT_USER\RestoreFromWindowsfilter\SOFTWARE" /s
# 比較の結果: 一致
# この操作を正しく終了しました。

・Delta Hive in Container
場所 : Q:\WcSandboxState\Hives\Software_Delta
参照元 : ?

ProcessMonitor のログで RegLoadKey されていた Hive。

Q:WcSandboxState は読み込み権限が無いので、適当に権限追加して開けるようにする ( Container 起動中は取ってこれないので一旦止める必要がある )。

上記 2 つと、Container 内から見える Registry を比較すると、Container Registry = System Hive in Container + Delta Hive in Container と分かった。

+
=

もし Base Hive in Top Image が下層 Layer の変更を全て merge した Snapshot であるとするなら、あとは Container 内で Copy on Write が実現できれば良い。

この Delta ファイルが Copy on Write の Copy と考えれば辻褄が合う。

● Registry Virtualization

実は、Registry には既に Copy on Write を実現している機能が存在する。
Windows Vista 以降、以下機能が追加されている。

Registry Virtualization

  • UAC 昇格前のアクセスを deny するのではなく、仮想の Registry Tree に Copy on Write する機能
  • ただし、仮想化が許されるのは HKEY_LOCAL_MACHINE\Software 以下のみ ?

完全に同じものではないようだが、似た原理は利用しているのかもしれない。

◆ 結論

これら情報を照らし合わせると、

  1. Container 起動
  2. Container 内にある C:\WcSandboxState\Hives\*\REGISTRY\WC\Silo + <Random GUID> に mount
  3. Container 内にある C:\Windows\System32\config\*\REGISTRY\WC\Silo + <Other Random GUID> に mount
  4. Container 内で Registry Write が発生すると、\REGISTRY\WC\Silo + <Random GUID> 以下に書き込まれる
  5. Container 内で Registry Read が発生すると、\REGISTRY\WC\Silo + <Random GUID>\REGISTRY\WC\Silo + <Other Random GUID> を merge した Virtual Registry から読み込む

と予測できる。

Network

次は、Network がどうなるか。
Network については、資料がとても充実している。

Network Namespace の実現方法は置いといて、まずは各 Docker Network の種類からみていく。

● nat

Host 上に仮想 NAT を置き、Container はその NAT 配下の Subnet に参加するという構成。

Container 間通信
外部 Outbound 通信
外部 Inbound 通信 ×

Docker Desktop を導入すると、Host に vEthernet (nat) という 仮想 NIC が作成される。

PS> Get-NetAdapter | ? {$_.Name -eq "vEthernet (nat)"} | ft -Property Name,MacAddress,DeviceID,DeviceName,InterfaceIndex,InterfaceName,InterfaceType,Virtual
# Name            MacAddress        DeviceID                               DeviceName                                     InterfaceIndex InterfaceName  InterfaceType Virtual
# ----            ----------        --------                               ----------                                     -------------- -------------  ------------- -------
# vEthernet (nat) 00-15-5D-4E-59-28 {9AAC8FFE-AD59-456E-A61F-F805BDD8E456} \Device\{9AAC8FFE-AD59-456E-A61F-F805BDD8E456}             81 ethernet_32779             6    True

PS> Get-NetIPAddress | ? {$_.InterfaceIndex -eq 81} | ft -Property IPAddress,InterfaceAlias,AddressFamily,PrefixLength,IPAddress,Type
# IPAddress                    InterfaceAlias  AddressFamily PrefixLength IPAddress                       Type
# ---------                    --------------  ------------- ------------ ---------                       ----
# fe80::3064:109f:dea2:c5be%81 vEthernet (nat)          IPv6           64 fe80::3064:109f:dea2:c5be%81 Unicast
# 172.26.16.1                  vEthernet (nat)          IPv4           20 172.26.16.1                  Unicast

PS> Get-NetNat
#
PS> Get-NetNatSession
# NatName                                 Protocol InternalSourceAddress InternalSourcePort InternalDestinationAddress InternalDestinationPort ExternalSourceAddress ExternalSourcePort ExternalDestinationAddress ExternalDestinationPort 
# -------                                 -------- --------------------- ------------------ -------------------------- ----------------------- --------------------- ------------------ -------------------------- -----------------------
# ICS9AAC8FFE-AD59-456E-A61F-F805BDD8E456        1          172.26.30.77                  1                    8.8.8.8                       1         192.168.100.5               1000                    8.8.8.8                    1000
# ...

WinNAT であるとするなら Get-NetNat で表示されるはずだが、なぜか空っぽだった。
しかし Get-NetNatSession にレコードはあるという謎挙動。

まぁ置いといて。
ExternalSourceAddress : 192.168.100.5 は Host の Default Root なので、NAPT されている事が分かる。

Docker からは以下のように認識される。

PS> docker network inspect 92320d040238
# [
#     {
#         "Name": "nat",
#         "Id": "92320d0402389650219d42455fd7ac317de0eb6411bad5a52bb84965ce90558a",
#         "Created": "2019-03-08T22:16:48.0365891+09:00",
#         "Scope": "local",
#         "Driver": "nat",
#         "EnableIPv6": false,
#         "IPAM": {
#             "Driver": "windows",
#             "Options": null,
#             "Config": [
#                 {
#                     "Subnet": "0.0.0.0/0"
#                 }
#             ]
#         },
#         "Internal": false,
#         "Attachable": false,
#         "Ingress": false,
#         "ConfigFrom": {
#             "Network": ""
#         },
#         "ConfigOnly": false,
#         "Containers": {
#             "d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286": {
#                 "Name": "wonderful_mahavira",
#                 "EndpointID": "2b416d30dd2957f57762790f14fcc03b238895231aac77e33cddfc6908fd7cf4",
#                 "MacAddress": "00:15:5d:4e:59:7d",
#                 "IPv4Address": "172.26.30.77/16",
#                 "IPv6Address": ""
#             },
#             ....
#         },
#         "Options": {
#             "com.docker.network.windowsshim.hnsid": "D637C6A7-6604-4888-9FD3-3372668AACB5",
#             "com.docker.network.windowsshim.networkname": "nat"
#         },
#         "Labels": {}
#     }
# ]

次は、Container 内から確認する。
Container を立ち上げると、

PS> docker run -it --isolation process mcr.microsoft.com/windows/servercore:1809 powershell.exe
PS> docker inspect d67 | wsl jq '.[0].NetworkSettings'
# {
#   "Bridge": "",
#   "SandboxID": "d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286",
#   "HairpinMode": false,
#   "LinkLocalIPv6Address": "",
#   "LinkLocalIPv6PrefixLen": 0,
#   "Ports": {},
#   "SandboxKey": "d67daa4ef881d5ee4ac428be26c06bfb9f98815a823693d583ad17b6d8f96286",
#   "SecondaryIPAddresses": null,
#   "SecondaryIPv6Addresses": null,
#   "EndpointID": "",
#   "Gateway": "",
#   "GlobalIPv6Address": "",
#   "GlobalIPv6PrefixLen": 0,
#   "IPAddress": "",
#   "IPPrefixLen": 0,
#   "IPv6Gateway": "",
#   "MacAddress": "",
#   "Networks": {
#     "nat": {
#       "IPAMConfig": null,
#       "Links": null,
#       "Aliases": null,
#       "NetworkID": "92320d0402389650219d42455fd7ac317de0eb6411bad5a52bb84965ce90558a",
#       "EndpointID": "2b416d30dd2957f57762790f14fcc03b238895231aac77e33cddfc6908fd7cf4",
#       "Gateway": "172.26.16.1",
#       "IPAddress": "172.26.30.77",
#       "IPPrefixLen": 16,
#       "IPv6Gateway": "",
#       "GlobalIPv6Address": "",
#       "GlobalIPv6PrefixLen": 0,
#       "MacAddress": "00:15:5d:4e:59:7d",
#       "DriverOpts": null
#     }
#   }
# }


(CONTAINER)> Get-NetAdapter | ft -Property Name,MacAddress,DeviceID,DeviceName,InterfaceIndex,InterfaceName,InterfaceType,Virtual
# Name                 MacAddress        DeviceID                               DeviceName InterfaceIndex InterfaceName InterfaceType Virtual
# ----                 ----------        --------                               ---------- -------------- ------------- ------------- -------
# vEthernet (Ethernet) 00-15-5D-4E-59-7D {06956E35-47F3-4029-8708-796120B3E527}                        87 iftype0_0                 0

(CONTAINER)> Get-NetIPAddress | ? {$_.InterfaceIndex -eq 87} | ft -Property IPAddress,InterfaceIndex,InterfaceAlias,AddressFamily,PrefixLength,IPA
# IPAddress                    InterfaceIndex InterfaceAlias       AddressFamily PrefixLength IPA
# ---------                    -------------- --------------       ------------- ------------ ---
# fe80::24cd:a697:b875:b711%87             87 vEthernet (Ethernet)          IPv6           64
# 172.26.30.77                             87 vEthernet (Ethernet)          IPv4           20

(CONTAINER)> Get-NetIPConfiguration | ? {$_.InterfaceIndex -eq 87} | ft -Property InterfaceIndex,@{Expression={$_.IPv4Address}},@{Expression={$_.IPv4DefaultGateway.NextHop}}
# InterfaceIndex $_.IPv4Address $_.IPv4DefaultGateway.NextHop
# -------------- -------------- -----------------------------
#             87 172.26.30.77   172.26.16.1

(CONTAINER)> Get-NetRoute -AddressFamily IPv4
# ifIndex DestinationPrefix          NextHop         RouteMetric ifMetric PolicyStore
# ------- -----------------          -------         ----------- -------- -----------
# 87      255.255.255.255/32         0.0.0.0                 256 5000     ActiveStore
# 86      255.255.255.255/32         0.0.0.0                 256 75       ActiveStore
# 87      224.0.0.0/4                0.0.0.0                 256 5000     ActiveStore
# 86      224.0.0.0/4                0.0.0.0                 256 75       ActiveStore
# 87      172.26.31.255/32           0.0.0.0                 256 5000     ActiveStore
# 87      172.26.30.77/32            0.0.0.0                 256 5000     ActiveStore
# 87      172.26.16.0/20             0.0.0.0                 256 5000     ActiveStore
# 86      127.255.255.255/32         0.0.0.0                 256 75       ActiveStore
# 86      127.0.0.1/32               0.0.0.0                 256 75       ActiveStore
# 86      127.0.0.0/8                0.0.0.0                 256 75       ActiveStore
# 87      0.0.0.0/0                  172.26.16.1             256 5000     ActiveStore

(CONTAINER)> Get-Netneighbor -State Stale
# ifIndex IPAddress                      LinkLayerAddress      State       PolicyStore
# ------- ---------                      ----------------      -----       -----------
# 87      fe80::c9e1:deb8:fbf1:9125      00-15-5D-4E-58-D0     Stale       ActiveStore
# 87      172.26.27.1                    00-15-5D-4E-58-D0     Stale       ActiveStore
# 87      172.26.16.1                    00-15-5D-4E-59-28     Stale       ActiveStore

(CONTAINER)> Get-NetFirewallRule
# Get-NetFirewallRule : There are no more endpoints available from the endpoint mapper.
# ...

Network Adapter や Routing Table, Arp Table が独立していることは分かった。
Firewall はエラーが出て確認できなかった。

Linux と違うのは、Windows Container は Network namespace を超えて Switch に直接接続できるところか。
Linux は veth のペアをそれぞれの Nemaspace に置くことで接続していた。

接続された Container 間はブリッジされアクセス可能で、Host や Internet への Outbound 通信は NAPT 経由で可能だ。
外部から Container への Inbound 通信は NAPT 超えでもしない限りできない。

Port Forwarding

次に、Port Forwarding について。
IIS の Image を Pull して、アクセスできるか試してみる。

PS> docker pull mcr.microsoft.com/windows/servercore/iis
# Using default tag: latest
# latest: Pulling from windows/servercore/iis
# 65014b3c3121: Already exists
# d48f50035439: Extracting [=====>                                             ]  74.09MB/620.8MB
# ...

PS> docker run -d --isolation process -p 8080:80 mcr.microsoft.com/windows/servercore/iis:latest
# 8f1055998ab5daff3d4e8d91f47e7742bf78b7e7b54d975b8d9e280088f7a5df
PS> docker inspect 8f105599 | wsl jq '.[0].NetworkSettings.Ports'
# {
#   "80/tcp": [
#     {
#       "HostIp": "0.0.0.0",
#       "HostPort": "8080"
#     }
#   ]
# }


上手くいった。

動作原理を探ってみる。
netsh の Port Proxy 辺りかなと思っていたが、

PS> netsh interface portproxy show v4tov4
#
PS> netstat -aon | findstr 8080
#

どうやら違うようだ。
じゃあ NAT の Port Forwarding かな、と思い見てみたが、

PS> Get-NetNatStaticMapping
#

何もない。

ICS (詳細後述) も同様に Port Forwarding 機能を持っているが、これも設定されていなかった。
https://superuser.com/questions/1241347/does-ics-allow-port-forwarding

その後も色々調べてみて、まさかと思い Routing Table を見てみたら、

PS> route print
# ...
#  固定ルート:
#   ネットワーク アドレス          ネットマスク  ゲートウェイ アドレス  メトリック
#           0.0.0.0          0.0.0.0      172.24.48.1     既定
# ...

怪しいやつを発見。
vEthernet (nat) が Gateway の乗っ取りをしているのかな。
ちなみに、Port Forward している Container を落とすと、このエントリは消える。

因みに Port Forwarding している Port にアクセスすると、NAT Session ができる。

192.168.100.104
$ curl http://192.168.100.5:8080
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
# <html xmlns="http://www.w3.org/1999/xhtml">
# <head>
# ...
PS> Get-NetNatSession
# NatName                    : ICS190A9C7A-6FC1-47F5-B6A7-8BBE2CB1FDD3
# InternalRoutingDomainId    : {b1062982-2b18-4b4f-b3d5-a78ddb9cdd49}
# CreationTime               : 2019/03/10 22:16:17 午後
# Protocol                   : 6
# InternalSourceAddress      : 172.24.58.96
# InternalSourcePort         : 80
# InternalDestinationAddress : 192.168.100.104
# InternalDestinationPort    : 52542
# ExternalSourceAddress      : 192.168.100.5
# ExternalSourcePort         : 8080
# ExternalDestinationAddress : 192.168.100.104
# ExternalDestinationPort    : 52542

NAT 名が 『ICS ~』 になっているのが気になるが…

ポツポツと要素は見えてきたが、それらがいまいち繋がらない。
もう少し掘り下げられそうなので、またあとで調べる。

● transparent

Host 上に作成された仮想 Switch に、Host 上の物理 NIC と Container 上の仮想 NIC をどちらも指すことで、Host の属する Subnet に参加させる構成。
いわゆる bridge 接続に近い構成が作れる。

Container 間通信
外部 Outbound 通信
外部 Inbound 通信

まずは、Network を作成する。

PS> docker network create -d transparent new_transparent
d80d77762f4fdf92f386cc9d0d40d273f0b225c9bbf3e1adfcef76e1c87dd8a5

PS> docker network inspect d80d77762f4f
# [
#     {
#         "Name": "new_transparent",
#         "Id": "d80d77762f4fdf92f386cc9d0d40d273f0b225c9bbf3e1adfcef76e1c87dd8a5",
#         "Created": "2019-03-12T23:13:07.251347+09:00",
#         "Scope": "local",
#         "Driver": "transparent",
#         "EnableIPv6": false,
#         "IPAM": {
#             "Driver": "windows",
#             "Options": {},
#             "Config": [
#                 {
#                     "Subnet": "0.0.0.0/0"
#                 }
#             ]
#         },
#         "Internal": false,
#         "Attachable": false,
#         "Ingress": false,
#         "ConfigFrom": {
#             "Network": ""
#         },
#         "ConfigOnly": false,
#         "Containers": {},
#         "Options": {
#             "com.docker.network.windowsshim.hnsid": "565C95F0-4D7B-40CA-849F-B70AB3FAB484"
#         },
#         "Labels": {}
#     }
# ]

Network Adapter には変化は無いが、Hyper-V には新たに仮想スイッチが追加されている。
しかし、VirtualBox の Host-Only に接続されていて、大丈夫か ? と思ったら案の定、Link Local Address が割り振られている。

PS> docker run -it --isolation process --net new_transparent mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)>  Get-NetIPAddress -AddressFamily IPv4 | ft
# ifIndex IPAddress                                       PrefixLength PrefixOrigin SuffixOrigin AddressState PolicyStore
# ------- ---------                                       ------------ ------------ ------------ ------------ -----------
# 49      169.254.130.120                                           16 WellKnown    Link         Preferred    ActiveStore
# 48      127.0.0.1                                                  8 WellKnown    WellKnown    Preferred    ActiveStore

Bind NIC

どうやら、物理 NIC が複数ある場合、com.docker.network.windowsshim.interface という Option で指定する必要がある らしい。

PS> docker network rm new_transparent
PS> docker network create -d transparent -o com.docker.network.windowsshim.interface="イーサネット" new_transparent
# d80d77762f4fdf92f386cc9d0d40d273f0b225c9bbf3e1adfcef76e1c87dd8a5

PS> docker run -it --isolation process --net new_transparent mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)>  Get-NetIPAddress -AddressFamily IPv4 | ft
# ifIndex IPAddress          PrefixLength PrefixOrigin SuffixOrigin AddressState PolicyStore
# ------- ---------          ------------ ------------ ------------ ------------ -----------
# 63      192.168.100.19               24 Dhcp         Dhcp         Preferred    ActiveStore
# 62      127.0.0.1                     8 WellKnown    WellKnown    Preferred    ActiveStore

DHCP から IP も取れている。

動作原理としては、Hyper-V の 『管理オペレーティングシステムにネットワークアダプタの共有を許可する』 という機能を利用しているようだ。

この機能を有効にすると、

  • 新たに仮想スイッチが作成される : <Docker Network ID> ( 上図で言うと 3b6b8ca... )
  • 物理 NIC が Hyper-V Extensible Virtual Switch という状態になる
    • 仮想スイッチ <Docker Network ID> の 1 ポートとして振る舞う
  • Host 用に仮想 NIC が1つ作られる : vEthernet (イーサネット)
  • vEthernet (イーサネット) が仮想スイッチ <Docker Network ID> に接続される
  • Container を立ち上げると仮想 NIC が作られ、仮想スイッチ <Docker Network ID> に接続される

となっている。
この『管理オペレーティング…』機能の詳細は こちらの記事 が参考になる。

Host も Container も上位 Router の管理する Network に直接所属し、Container 間通信はもちろん、別 Subnet との通信もこの上位 Router によって管理され、可能となる。

VLAN

作成時に VLAN の Tag 指定 もでき、それを使えば Network を分離することもできる。

PS> docker network create -d transparent -o com.docker.network.windowsshim.vlanid=11 -o com.docker.network.windowsshim.interface="イーサネット" vlan_11
PS> docker network create -d transparent -o com.docker.network.windowsshim.vlanid=12 -o com.docker.network.windowsshim.interface="イーサネット" vlan_12

この際、物理 NIC Port は Trunk mode となり、全ての Tag を通す Port となる。

● l2bridge

主に SDN ( Software Defined Network ) での利用を目的とした Driver。
transparent と似た構成を取るが、仮想 Switch に VFP ( Virtual Filtering Platform ) という Extension が含まれている のが特徴。

この VFP により、

  • Mac Address Rewrite 機能
    • Container の Inbound/Outbound フレームヘッダの MAC Address を、Host の vEthernet (イーサネット) と同じものに書き換え
  • Network Controller による Network policy のリモート制御
    • Port ACLs
    • Encapsulation
    • QoS 制御

等の機能拡張が行われているらしい。
https://blogs.technet.microsoft.com/virtualization/2016/05/05/windows-container-networking/
https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-isolation-security

SDN 環境を用意するのは辛いので、取り敢えず Local で作って、分かる範囲で調べる。
参考リンク によると、DHCP に対応していないようで、 subnet と gateway を明示する必要があるらしい。

PS> docker network create -d l2bridge -o com.docker.network.windowsshim.interface="イーサネット" --subnet=192.168.100.0/24 --gateway=192.168.100.254 l2b
# 11df7738042939bd3c10109ea8315304a914d8811e74f05916d52ec4dc94a20f

PS> docker network inspect 11df7738042
# [
#     {
#         "Name": "l2b",
#         "Id": "11df7738042939bd3c10109ea8315304a914d8811e74f05916d52ec4dc94a20f",
#         "Created": "2019-03-13T00:32:10.8494309+09:00",
#         "Scope": "local",
#         "Driver": "l2bridge",
#         "EnableIPv6": false,
#         "IPAM": {
#             "Driver": "windows",
#             "Options": {},
#             "Config": [
#                 {
#                     "Subnet": "192.168.100.0/24",
#                     "Gateway": "192.168.100.254"
#                 }
#             ]
#         },
#         "Internal": false,
#         "Attachable": false,
#         "Ingress": false,
#         "ConfigFrom": {
#             "Network": ""
#         },
#         "ConfigOnly": false,
#         "Containers": {},
#         "Options": {
#             "com.docker.network.windowsshim.hnsid": "5C46A2B6-666C-4467-A06F-140262C92027",
#             "com.docker.network.windowsshim.interface": "イーサネット"
#         },
#         "Labels": {}
#     }
# ]

Container を立ち上げる。

PS> docker run -it --isolation process --net l2b mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)>  Get-NetIPAddress -AddressFamily IPv4 | ft
# ifIndex IPAddress                                       PrefixLength PrefixOrigin SuffixOrigin AddressState PolicyStore
# ------- ---------                                       ------------ ------------ ------------ ------------ -----------
# 45      192.168.100.207                                           24 Manual       Manual       Preferred    ActiveStore
# 44      127.0.0.1                                                  8 WellKnown    WellKnown    Preferred    ActiveStore

この時に作られた仮想 Switch を Hyper-V Manager から見てみると『Microsoft Azure VFP Switch Extension』が有効になっているのが分かる。
screenshot_42.png

MAC Address Rewrite

どのように MAC Address が書き換わるのかを確認する。
Container を参加させた Subnet 内の適当な PC へ Packet を送信し、その時の Arp Table の状態を確認してみる。

Host
PS> Get-NetAdapter | ? { $_.Name -eq "イーサネット" } | fl -Property MacAddress
# MacAddress : AD-5C-E2-55-79-1A

PS> ping 192.168.100.150
# 192.168.100.150 に ping を送信しています 32 バイトのデータ:
# 192.168.100.150 からの応答: バイト数 =32 時間 =12ms TTL=118
# 192.168.100.150 からの応答: バイト数 =32 時間 =9ms TTL=118
# ...

PS> Get-NetNeighbor | ? { $_.IPAddress -eq "192.168.100.150" }
# ifIndex IPAddress        LinkLayerAddress      State       PolicyStore
# ------- ---------        ----------------      -----       -----------
# 25      192.168.100.150    D4-31-8E-5A-B2-1C     Reachable   ActiveStore
Container
PS> Get-NetAdapter | fl -Property MacAddress
# MacAddress : 00-15-5D-66-7D-5E

PS> ping 192.168.100.150
# Pinging 192.168.100.150 with 32 bytes of data:
# Reply from 192.168.100.150: bytes=32 time=1ms TTL=64
# Reply from 192.168.100.150: bytes=32 time<1ms TTL=64
# ...

PS> Get-NetNeighbor | ? { $_.IPAddress -eq "192.168.100.150" }
# ifIndex IPAddress        LinkLayerAddress      State       PolicyStore
# ------- ---------        ----------------      -----       -----------
# 45      192.168.100.150    D4-31-8E-5A-B2-1C     Reachable   ActiveStore
192.168.100.150
$ ip link show eth0                                                                                                                  # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
#     link/ether d4:31:8e:5a:b2:ic brd ff:ff:ff:ff:ff:ff

$ ip n | grep -e 192.168.100.5  -e 192.168.100.207 
# 192.168.100.207 dev eth0 lladdr ad:5c:e2:55:79:1a STALE
# 192.168.100.5 dev eth0 lladdr ad:5c:e2:55:79:1a STALE

どちらも Host の MAC Address になっている。

正直、MAC Address を書き換える必要性は分からないが、Port ACLs 辺りに関係するのだろうか。

Kubernetes

l2bridge は、Kubernetes の L3 Routing Topology や Host Gateway Mode などで利用される。
https://kubernetes.io/docs/getting-started-guides/windows/#upstream-l3-routing-topology
https://kubernetes.io/docs/getting-started-guides/windows/#host-gateway-topology

● l2tunnel

基本的には l2bridge と同じだが、同一 Host & 同一 Subnet の場合に、l2bridge は Container 間で Bridge 通信を行うが、l2tunnel は全てのパケットを一度必ず Host の物理 NIC に送るらしい。

この場合の違いとして、l2bridge は Network policy に引っかからないが、l2tunnel は Network policy が適用される。
https://docs.microsoft.com/en-US/windows-server/networking/sdn/manage/connect-container-endpoints-to-a-tenant-virtual-network

● overlay

Docker Swarm や Kubernetes 向けの Driver。

Swarm Mode

まずは Docker を Swarm Mode に切り替える。
https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/swarm-mode

PS> docker swarm init --advertise-addr=192.168.100.9 --listen-addr 192.168.100.9:2377
# Swarm initialized: current node (bgh6nt297dyjay8e5pkf6c5pn) is now a manager.
# 
# To add a worker to this swarm, run the following command:
#     docker swarm join --token SWMTKN-1-3g8mm9neiee9y4tpj23azhmi964s7pbb33czbe2f7h6jwdiz2t-hkhb33as2k9i3usr823xnj3z5 192.168.100.9:2377
# 
# To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

PS> docker node ls
# ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
# bgh6nt297dyjay8e5pkf6c5pn *   HOST_MACHINE        Ready               Active              Leader              18.09.3

PS> docker info --format '{{json .}}' | wsl jq '.Swarm'
# {
#   "NodeID": "bgh6nt297dyjay8e5pkf6c5pn",
#   "NodeAddr": "192.168.100.9",
#   "LocalNodeState": "active",
#   "ControlAvailable": true,
#   "Error": "",
#   "RemoteManagers": [
#     {
#       "NodeID": "bgh6nt297dyjay8e5pkf6c5pn",
#       "Addr": "192.168.10.9:2377"
#     }
#   ],
#   "Nodes": 1,
#   "Managers": 1,
#   "Cluster": {
#     "ID": "czxk5zcetu8i8axfedhpg4wrx",
#     "Version": {
#       "Index": 9
#     },
#     "CreatedAt": "2019-03-13T21:08:00.8401934Z",
#     "UpdatedAt": "2019-03-13T21:08:01.4015433Z",
#     "Spec": {
#       "Name": "default",
#       "Labels": {},
#       "Orchestration": {
#         "TaskHistoryRetentionLimit": 5
#       },
#       "Raft": {
#         "SnapshotInterval": 10000,
#         "KeepOldSnapshots": 0,
#         "LogEntriesForSlowFollowers": 500,
#         "ElectionTick": 10,
#         "HeartbeatTick": 1
#       },
#       "Dispatcher": {
#         "HeartbeatPeriod": 5000000000
#       },
#       "CAConfig": {
#         "NodeCertExpiry": 7776000000000000
#       },
#       "TaskDefaults": {},
#       "EncryptionConfig": {
#         "AutoLockManagers": false
#       }
#     },
#     "TLSInfo": {
#       "TrustRoot": "...",
#       "CertIssuerSubject": "...",
#       "CertIssuerPublicKey": "..."
#     },
#     "RootRotationInProgress": false,
#     "DefaultAddrPool": [
#       "10.0.0.0/8"
#     ],
#     "SubnetSize": 24
# }

すると勝手に ingress という Docker Network が作られる。こいつは Get-NetAdapter にも Get-VMSwitch にも現れない。

PS> docker network inspect ingress
# [
#     {
#         "Name": "ingress",
#         "Id": "vvbuty8emqhj8pjdeisgz60t1",
#         "Created": "2019-03-13T21:08:00.8401934Z",
#         "Scope": "swarm",
#         "Driver": "overlay",
#         "EnableIPv6": false,
#         "IPAM": {
#             "Driver": "default",
#             "Options": null,
#             "Config": [
#                 {
#                     "Subnet": "10.255.0.0/16",
#                     "Gateway": "10.255.0.1"
#                 }
#             ]
#         },
#         "Internal": false,
#         "Attachable": false,
#         "Ingress": true,
#         "ConfigFrom": {
#             "Network": ""
#         },
#         "ConfigOnly": false,
#         "Containers": null,
#         "Options": {
#             "com.docker.network.driver.overlay.vxlanid_list": "4096"
#         },
#         "Labels": null
#     }
# ]

今は無視して、Overlay Driver の network を独自に作る。
ingress 同様、Get-NetAdapter にも Get-VMSwitch にも現れないので実体は不明。

PS> docker network create --driver=overlay other_overlay
wi88on1q4q0l0nsca5uie488l

PS> docker network inspect wi88on1q4q0l0nsca5uie488l
# [
#     {
#         "Name": "other_overlay",
#         "Id": "wi88on1q4q0l0nsca5uie488l",
#         "Created": "2019-03-13T21:23:35.0489805Z",
#         "Scope": "swarm",
#         "Driver": "overlay",
#         "EnableIPv6": false,
#         "IPAM": {
#             "Driver": "default",
#             "Options": null,
#             "Config": [
#                 {
#                     "Subnet": "10.0.0.0/24",
#                     "Gateway": "10.0.0.1"
#                 }
#             ]
#         },
#         "Internal": false,
#         "Attachable": false,
#         "Ingress": false,
#         "ConfigFrom": {
#             "Network": ""
#         },
#         "ConfigOnly": false,
#         "Containers": null,
#         "Options": {
#             "com.docker.network.driver.overlay.vxlanid_list": "4097"
#         },
#         "Labels": null
#     }
# ]

IIS を Service として追加する。

PS> docker service create --name=web --endpoint-mode dnsrr --network=ingress  mcr.microsoft.com/windows/servercore/iis:latest
# Error response from daemon: rpc error: code = InvalidArgument desc = Service cannot be explicitly attached to the ingress network "ingress"

なんか、ingress は自由に使えないっぽいので、自作の方を使う。

PS> docker service create --name=web --isolation=process  --endpoint-mode=dnsrr --network=other_overlay  mcr.microsoft.com/windows/servercore/iis:latest
# jzp3mme7gtsar9hxj3k28t6hc
# overall progress: 0 out of 1 tasks
# 1/1: hnsCall failed in Win32: The parameter is incorrect. (0x57)
# 
#    kill by Ctrl+C

PS> docker service ls
# ID                  NAME                MODE                REPLICAS            IMAGE                                             PORTS
# jzp3mme7gtsa        web                 replicated          0/1                 mcr.microsoft.com/windows/servercore/iis:latest
PS> docker service ps web
# docker service ps web
# ID                  NAME                IMAGE                                             NODE                DESIRED STATE       CURRENT STATE                    ERROR                              PORTS
# jisa93cfywpm        web.1               mcr.microsoft.com/windows/servercore/iis:latest                       Ready               Pending less than a second ago
# oiz4kx611k0p         \_ web.1           mcr.microsoft.com/windows/servercore/iis:latest   HOST_MACHINE        Shutdown            Rejected 2 seconds ago           "hnsCall failed in Win32: The …"
# ms9ikb3tur0t         \_ web.1           mcr.microsoft.com/windows/servercore/iis:latest   HOST_MACHINE        Shutdown            Rejected 7 seconds ago           "hnsCall failed in Win32: The …"
# 2wg539hb1xhc         \_ web.1           mcr.microsoft.com/windows/servercore/iis:latest   HOST_MACHINE        Shutdown            Rejected 13 seconds ago          "hnsCall failed in Win32: The …"
# xrn0ntwpup47         \_ web.1           mcr.microsoft.com/windows/servercore/iis:latest   HOST_MACHINE        Shutdown            Rejected 18 seconds ago          "hnsCall failed in Win32: The …"
# thdieu0w7074         \_ web.1           mcr.microsoft.com/windows/servercore/iis:latest   HOST_MACHINE        Shutdown            Rejected 20 seconds ago          "hnsCall failed in Win32: The …"
PS> docker service rm web

今度は謎のエラーが出て永遠に終わらず、強制終了しても Replicas が 1 にならない。Option を変えても、自作の overlay を使う限り、同じエラーが出続ける。公開ポートモード でやっても同様。
とにかく、overlay network を利用しようとするとエラーが出るらしい。

log.txt
[00:40:46.380][WindowsDaemon  ][Info   ] debug: releasing IPv4 pools from network ingress (soghasemncn07kmzqtbsgew0z)
[00:40:46.381][WindowsDaemon  ][Info   ] debug: ReleaseAddress(LocalDefault/10.255.0.0/16, 10.255.0.1)
[00:40:46.383][WindowsDaemon  ][Info   ] debug: Released address PoolID:LocalDefault/10.255.0.0/16, Address:10.255.0.1 Sequence:App: ipam/default/data, ID: LocalDefault/10.255.0.0/16, DBIndex: 0x0, Bits: 65536, Unselected: 65533, Sequence: (0xc0000000, 1)->(0x0, 2046)->(0x1, 1)->end Curr:0
[00:40:46.384][WindowsDaemon  ][Info   ] debug: ReleasePool(LocalDefault/10.255.0.0/16)
[00:40:46.385][WindowsDaemon  ][Error  ] fatal task error [task.id=gy02uficooig3h0zuq7zxajmx error=hnsCall failed in Win32: The parameter is incorrect. (0x57) service.id=3b25cd9gd87khs77wjbhj8gkm module=node/agent/taskmanager node.id=k1vkebradiz1er039xb5u9vbk]

調べてみると、同様の現象が報告されている。未解決。

エラーの内容的に、libnetwork, hnsproxy.dll 辺りの API Call に齟齬があるとするなら、今は諦めるしかないか。

● ics

参考リンク に情報は無いが、Default で作成される謎の Driver。

PS> docker network ls
# NETWORK ID          NAME                DRIVER              SCOPE
# 521a6bc421a4        Default Switch      ics                 local
# bc9eed69c95b        nat                 nat                 local
# 919a6b63caa3        none                null                local

そもそも ICS ( Internet Connection Sharing ) とは Windows が元々持っている機能。
ICS 設定された NIC が NAT のような役割をすることで、LAN 内の他の PC が ICS 設定した PC を介して Internet へアクセスすることが出来るというもの。

Default Switch の実体は何か探すと、仮想 Switch に同名のものがある。

Default Switch は Hyper-V 導入時作成されて、『規定のスイッチ』として設定されている。
また、Default Switch には、vEthernet ( Default Switch ) という仮想 NIC が刺さっている。
Default Switch network の Gateway がこの仮想 NIC を指すので、おそらく間違いない。

PS>  Get-NetIPConfiguration | ? {$_.InterfaceAlias -eq "vEthernet (Default Switch)"} | fl -Property IPv4Address
# IPv4Address : {172.17.120.129}

PS> docker network inspect "Default Switch" | wsl jq '.[0].IPAM.Config'
# [
#   {
#     "Subnet": "172.17.120.128/28",
#     "Gateway": "172.17.120.129"
#   }
# ]

では、どのように設定されているのか、 ICS 設定を見てみると … ICS になってない !

まさかと思い、Container を立ち上げて、NAT 的動きをしていないか調べてみると、

PS> docker run -it --isolation process --net "Default Switch" mcr.microsoft.com/windows/servercore:1809 powershell.exe
(CONTAINER)> ping 8.8.8.8
PS> Get-NetNatSession
# NatName                    : ICSb28ea085-cad9-4498-9e07-30fe2d83e5bc
# InternalRoutingDomainId    : {b1062982-2b18-4b4f-b3d5-a78ddb9cdd49}
# CreationTime               : 2019/03/10 5:42:13 午前
# Protocol                   : 1
# InternalSourceAddress      : 172.18.1.12
# InternalSourcePort         : 1
# InternalDestinationAddress : 8.8.8.8
# InternalDestinationPort    : 1
# ExternalSourceAddress      : 192.168.100.5
# ExternalSourcePort         : 1000
# ExternalDestinationAddress : 8.8.8.8
# ExternalDestinationPort    : 1000

ん ? やっぱり nat なの ?

おそらく、Hyper-V は以前は仮想 NAT が作れなかったので、NAT Like な Alternative として ICS を利用していたが、現在は仮想 NAT が作れるので Default Switch が NAT として作られるようになったのではないだろうか ( 適当 )。

※ 追記
そもそも最初から Default Switch は NAT だったらしいです。
Docker Network がそれをなぜ ics と認識しているのかは依然として謎。
参考: 謎: Windows 10 ver 1809 の Default Switch の NAT サブネットが起動ごとに変わる件 - 山市良のえぬなんとかわーるど

その他

Environment

System 環境変数は HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment にあり、Registry の分離がなされていれば自然と分離されるようだ。

User 環境変数は HKU\<<SID>>\Environment にあり、これも同様と考えられる。

感想

Windows について、無駄に詳しくなった。
しかし、Windows は Linux とは違い、最終的には闇の中 になってしまうのは、しょうがないのかなぁ。

次回は、Hyper-V Isolation と LCOW をまとめる。

おまけ

Windows : Volume GUID から、乗っている Disc を探す方法

diskpart はなぜか Volume の GUID を出してくれないので、代わりに diskext で確認する。

PS> diskext
# ...
# Volume: \\?\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}\
#    Mounted at: <unmounted>
#    Extent [1]:
#        Disk:   2
#        Offset: 135266304
#        Length: 21339553280
# ...

これで、Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b} に対応する Disk が 2 であることが分かる。
あとは、diskpart で Disk 2 について調べると良い。

参考

https://www.slideshare.net/Docker/windows-container-security
https://www.slideshare.net/Docker/windows-server-and-docker-the-internals-behind-bringing-docker-and-containers-to-windows-by-taylor-brown-and-john-starks
https://blogs.msdn.microsoft.com/microsoft_press/2017/08/30/free-ebook-introduction-to-windows-containers/
https://www.slideshare.net/Docker/windows-container-security
https://www.slideshare.net/kazukitakai/windows-server-2019-container
https://www.slideshare.net/firewood/ss-40143470
https://yamanxworld.blogspot.com/
https://www.itmedia.co.jp/author/208420/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

以前、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 を一旦リネームすると、新たに作り直されて解決する。

参考

https://etogen.hatenablog.com/entry/2018/07/30/220805

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerに統合されたBuildKitのLLB (low-level builder)の仕様を探ってみよう

こんにちは。po3rin です。今回は前回の記事で解説し損ねた「そもそものLLBの中身はどうなってんねん」というところを解説します。

そもそもLLBとは

BuildKit は、LLB というプロセスの依存関係グラフを定義するために使用されるバイナリ中間言語を利用して。イメージをビルドしています。

なぜこの中間言語を挟むかというと、LLB は DAG 構造(上の画像のような非循環構造)を取ることにより、ステージごとの依存を解決し、並列実行可能な形で記述可能だからです。これにより、BuildKit を使った docker build は並列実行を可能にしています。

参考: Buildkit の Goのコードを読んで Dockerfile 抽象構文木から LLB を生成するフローを覗いてみよう!!

LLB の構造を覗く

まずはLLBの中身を探るために簡単なDockerfileからLLBに変換します。ここではLLBの構造を見やくするためにbuildctlコマンドを使います。

buildctl を使えるようにする

インストールはこちらを参考にしてみて下さい。

参考: Docker に正式統合された BuildKit の buildctl コマンドの実行環境構築

buildctl でLLBを確認する

まずはDockerfileからLLBを生成して構造を目視で確認しましょう。まずは今回のターゲットになるDockerfileです。

FROM golang:1.12 AS stage0

ENV GO111MODULE on
WORKDIR /go

ADD ./po-go /go
RUN go build -o go_bin

そしてGo言語の環境で下記を実行します。Go言語が分からなくても安心してください。Dockerfileを読み込んでbuildkitが提供する関数でLLBに変換して、見やすい形で標準出力に出しているだけです。

package main

import (
    "io/ioutil"
    "os"

    "github.com/moby/buildkit/client/llb"
    "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb"
    "github.com/moby/buildkit/util/appcontext"
)

func main() {
    df, _ := ioutil.ReadFile("./Dockerfile")

    st, _, _ := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{})

    def, _ := st.Marshal()
    llb.WriteTo(def, os.Stdout)
}

実行します。

go run main.go | buildctl debug dump-llb | jq .

出力は長いので折り畳んでおきます。

LLBのJSON
{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "docker-image://docker.io/docker/dockerfile-copy:v0.1.9@sha256:e8f159d3f00786604b93c675ee2783f8dc194bb565e61ca5788f6a6e9d304061",
        "attrs": {
          "image.recordtype": "internal"
        }
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b",
  "OpMetadata": {
    "description": {
      "llb.customname": "[internal] helper image for file operations"
    },
    "caps": {
      "source.image": true
    }
  }
}
{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "local://context",
        "attrs": {
          "local.followpaths": "[\"po-go\"]",
          "local.sharedkeyhint": "context",
          "local.unique": "0vlmkjq7ccgxx0c2pmhknu7gr"
        }
      }
    },
    "constraints": {}
  },
  "Digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa",
  "OpMetadata": {
    "description": {
      "llb.customname": "[internal] load build context"
    },
    "caps": {
      "source.local": true,
      "source.local.followpaths": true,
      "source.local.sharedkeyhint": true,
      "source.local.unique": true
    }
  }
}
{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "docker-image://docker.io/library/golang:1.12"
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "FROM golang:1.12",
      "llb.customname": "[1/3] FROM docker.io/library/golang:1.12"
    },
    "caps": {
      "source.image": true
    }
  }
}
{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b",
        "index": 0
      },
      {
        "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
        "index": 0
      },
      {
        "digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa",
        "index": 0
      }
    ],
    "Op": {
      "Exec": {
        "meta": {
          "args": [
            "copy",
            "--unpack",
            "/src-0/po-go",
            "go"
          ],
          "cwd": "/dest"
        },
        "mounts": [
          {
            "input": 0,
            "dest": "/",
            "output": -1,
            "readonly": true
          },
          {
            "input": 1,
            "dest": "/dest",
            "output": 0
          },
          {
            "input": 2,
            "selector": "./po-go",
            "dest": "/src-0/po-go",
            "output": -1,
            "readonly": true
          }
        ]
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "ADD ./po-go /go",
      "llb.customname": "[2/3] ADD ./po-go /go"
    },
    "caps": {
      "exec.meta.base": true,
      "exec.mount.bind": true,
      "exec.mount.selector": true
    }
  }
}
{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c",
        "index": 0
      }
    ],
    "Op": {
      "Exec": {
        "meta": {
          "args": [
            "/bin/sh",
            "-c",
            "go build -o go_bin"
          ],
          "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "GO111MODULE=on"
          ],
          "cwd": "/go"
        },
        "mounts": [
          {
            "input": 0,
            "dest": "/",
            "output": 0
          }
        ]
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "RUN go build -o go_bin",
      "llb.customname": "[3/3] RUN go build -o go_bin"
    },
    "caps": {
      "exec.meta.base": true,
      "exec.mount.bind": true
    }
  }
}
{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40",
        "index": 0
      }
    ],
    "Op": null
  },
  "Digest": "sha256:33db869df4ee6fae4e29c22dcf328725c6ca6d20470680be6c6b0adf7e14cf3e",
  "OpMetadata": {
    "caps": {
      "constraints": true,
      "meta.description": true,
      "platform": true
    }
  }
}

LLBの中身をformat定義と比べながら見ていく

LLBの仕様はprotobufで定義されています。
https://github.com/moby/buildkit/blob/master/solver/pb/ops.proto

先ほど説明した通り、LLBは有向非循環グラフの構造をとります。すなわち、ここで定義されているのは主に各ノード(接点。頂点)のデータフォーマット(Op)とノードを枝(エッジ)を表現するータフォーマット(Definition)です。まず基本的なところから見ていきましょう。Opはグラフ構造のノードを表しています。

// Op represents a vertex of the LLB DAG.
message Op {
    // inputs is a set of input edges.
    repeated Input inputs = 1;
    oneof op {
        ExecOp exec = 2;
        SourceOp source = 3;
        CopyOp copy = 4;
        BuildOp build = 5;
    }
    Platform platform = 10;
    WorkerConstraints constraints = 11;
}

これは実際のLLBでは下の部分に対応します。FROM golang:1.12の一行がこのようにOpに変換されていることがわかります。

{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "docker-image://docker.io/library/golang:1.12"
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "FROM golang:1.12",
      "llb.customname": "[1/3] FROM docker.io/library/golang:1.12"
    },
    "caps": {
      "source.image": true
    }
  }
}

見るとわかる通り、Docker系の情報はOpMetadataの中にメタデータとして格納されるだけです。よってLLBがDockerfileの構造だけに依存している訳ではないことがわかります。

ここらでOpの種類を見てみましょう。Opの定義のoneof opを見るとわかる通り、実はOpの種類はわずか4種類しかありません。例えばExecOpは下記の部分です。

"Op": {
    "inputs": [
      // ...
    ],
    "Op": {
      "Exec": {
        "meta": {
          "args": [
            "/bin/sh",
            "-c",
            "go build -o go_bin"
          ],
          "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "GO111MODULE=on"
          ],
          "cwd": "/go"
        },
        "mounts": [
          {
            "input": 0,
            "dest": "/",
            "output": 0
          }
        ]
      }
    },
    "platform": {
      // ...
    },
    "constraints": {}
  },
  "Digest": "sha256:f2dd48bf7656705cfbefa478dca927bbe298d9adabd39576bfb7131ff57fd261",
  "OpMetadata": {
    // ...
  }

meta情報を見ると、argsにRUNで指定したコマンドが、cwdにWORKDIRの値が、envにENVで指定した環境変数がセットされています。実は、LLBの世界での環境変数ENVはExecOpだけに紐づいており、全てのOpに共通した設定というのができない構造になっています。

LLBが有向非循環構造になっているのを確認する

さてLLBの構成因子Opを確認したところでこれらが有向非循環グラフになっていることを確認しましょう。ポイントはdigestです。もう一度LLBを見てみましょう。

{
  "Op": {
    "Op": {
      // ...
  },
  "Digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5",
  "OpMetadata": {
    // ...
  }
}

Opには一つdigestがセットされています。一方でこのOpに依存するOpをみてみましょう.

{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b",
        "index": 0
      },
      {
        "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
        "index": 0
      },
      {
        "digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5",
        "index": 0
      }
    ],
    "Op": {
      // ...
    },
    "platform": {
      // ...
    },
    "constraints": {}
  },
  "Digest": "sha256:d9fce5f43b1af2d2b9192de6eae55a2ea79a190c058cb9cfddb1af3ffbdb21d2",
  "OpMetadata": {
    // ...
    }
  }
}

inputsとしてエッジで紐付くOpのdigestの値が記載されています。このようにしてLLBはDAG構造を表現しています。例えば。下記のようなDockerfileは

FROM golang:1.12 AS stage0
WORKDIR /go
ADD ./ /go
RUN go build -o stage0_bin

FROM golang:1.12 AS stage1
WORKDIR /go
ADD ./ /go
RUN go build -o stage1_bin

FROM golang:1.12
COPY --from=stage0 /go/stage0_bin /
COPY --from=stage1 /go/stage1_bin /

digest値を追っていくと下記のようなDAG構造を取っていることがわかります。

なぜLLBを挟むことで並列化を実現できるかが一目でわかりますね。

まとめ

簡単にLLBの構造を追ってみて、なぜBuildKitでビルドの並列化ができるのかをみていきました。docker build の内部的な理解や、buildkikのコードリーディングをする際などに役立つはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerに統合されたBuildKitのLLB (low-level builder)の仕様を探ってみよう。

こんにちは。po3rin です。今回は前回の記事で解説し損ねた「そもそものLLBの中身はどうなってんねん」というところを解説します。

そもそもLLBとは

BuildKit は、LLB というプロセスの依存関係グラフを定義するために使用されるバイナリ中間言語を利用して。イメージをビルドしています。

なぜこの中間言語を挟むかというと、LLB は DAG 構造(上の画像のような非循環構造)を取ることにより、ステージごとの依存を解決し、並列実行可能な形で記述可能だからです。これにより、BuildKit を使った docker build は並列実行を可能にしています。

参考: Buildkit の Goのコードを読んで Dockerfile 抽象構文木から LLB を生成するフローを覗いてみよう!!

LLB の構造を覗く

まずはLLBの中身を探るために簡単なDockerfileからLLBに変換します。ここではLLBの構造を見やくするためにbuildctlコマンドを使います。

buildctl を使えるようにする

インストールはこちらを参考にしてみて下さい。

参考: Docker に正式統合された BuildKit の buildctl コマンドの実行環境構築

buildctl でLLBを確認する

まずはDockerfileからLLBを生成して構造を目視で確認しましょう。まずは今回のターゲットになるDockerfileです。

FROM golang:1.12 AS stage0

ENV GO111MODULE on
WORKDIR /go

ADD ./po-go /go
RUN go build -o go_bin

そしてGo言語の環境で下記を実行します。Go言語が分からなくても安心してください。Dockerfileを読み込んでbuildkitが提供する関数でLLBに変換して、見やすい形で標準出力に出しているだけです。

package main

import (
    "io/ioutil"
    "os"

    "github.com/moby/buildkit/client/llb"
    "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb"
    "github.com/moby/buildkit/util/appcontext"
)

func main() {
    df, _ := ioutil.ReadFile("./Dockerfile")

    st, _, _ := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{})

    def, _ := st.Marshal()
    llb.WriteTo(def, os.Stdout)
}

実行します。

go run main.go | buildctl debug dump-llb | jq .

出力は長いので折り畳んでおきます。

LLBのJSON
{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "docker-image://docker.io/docker/dockerfile-copy:v0.1.9@sha256:e8f159d3f00786604b93c675ee2783f8dc194bb565e61ca5788f6a6e9d304061",
        "attrs": {
          "image.recordtype": "internal"
        }
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b",
  "OpMetadata": {
    "description": {
      "llb.customname": "[internal] helper image for file operations"
    },
    "caps": {
      "source.image": true
    }
  }
}
{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "local://context",
        "attrs": {
          "local.followpaths": "[\"po-go\"]",
          "local.sharedkeyhint": "context",
          "local.unique": "0vlmkjq7ccgxx0c2pmhknu7gr"
        }
      }
    },
    "constraints": {}
  },
  "Digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa",
  "OpMetadata": {
    "description": {
      "llb.customname": "[internal] load build context"
    },
    "caps": {
      "source.local": true,
      "source.local.followpaths": true,
      "source.local.sharedkeyhint": true,
      "source.local.unique": true
    }
  }
}
{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "docker-image://docker.io/library/golang:1.12"
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "FROM golang:1.12",
      "llb.customname": "[1/3] FROM docker.io/library/golang:1.12"
    },
    "caps": {
      "source.image": true
    }
  }
}
{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b",
        "index": 0
      },
      {
        "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
        "index": 0
      },
      {
        "digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa",
        "index": 0
      }
    ],
    "Op": {
      "Exec": {
        "meta": {
          "args": [
            "copy",
            "--unpack",
            "/src-0/po-go",
            "go"
          ],
          "cwd": "/dest"
        },
        "mounts": [
          {
            "input": 0,
            "dest": "/",
            "output": -1,
            "readonly": true
          },
          {
            "input": 1,
            "dest": "/dest",
            "output": 0
          },
          {
            "input": 2,
            "selector": "./po-go",
            "dest": "/src-0/po-go",
            "output": -1,
            "readonly": true
          }
        ]
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "ADD ./po-go /go",
      "llb.customname": "[2/3] ADD ./po-go /go"
    },
    "caps": {
      "exec.meta.base": true,
      "exec.mount.bind": true,
      "exec.mount.selector": true
    }
  }
}
{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c",
        "index": 0
      }
    ],
    "Op": {
      "Exec": {
        "meta": {
          "args": [
            "/bin/sh",
            "-c",
            "go build -o go_bin"
          ],
          "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "GO111MODULE=on"
          ],
          "cwd": "/go"
        },
        "mounts": [
          {
            "input": 0,
            "dest": "/",
            "output": 0
          }
        ]
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "RUN go build -o go_bin",
      "llb.customname": "[3/3] RUN go build -o go_bin"
    },
    "caps": {
      "exec.meta.base": true,
      "exec.mount.bind": true
    }
  }
}
{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40",
        "index": 0
      }
    ],
    "Op": null
  },
  "Digest": "sha256:33db869df4ee6fae4e29c22dcf328725c6ca6d20470680be6c6b0adf7e14cf3e",
  "OpMetadata": {
    "caps": {
      "constraints": true,
      "meta.description": true,
      "platform": true
    }
  }
}

LLBの中身をformat定義と比べながら見ていく

LLBの仕様はprotobufで定義されています。
https://github.com/moby/buildkit/blob/master/solver/pb/ops.proto

先ほど説明した通り、LLBは有向非循環グラフの構造をとります。すなわち、ここで定義されているのは主に各ノード(接点。頂点)のデータフォーマット(Op)とノードを枝(エッジ)を表現するータフォーマット(Definition)です。まず基本的なところから見ていきましょう。Opはグラフ構造のノードを表しています。

// Op represents a vertex of the LLB DAG.
message Op {
    // inputs is a set of input edges.
    repeated Input inputs = 1;
    oneof op {
        ExecOp exec = 2;
        SourceOp source = 3;
        CopyOp copy = 4;
        BuildOp build = 5;
    }
    Platform platform = 10;
    WorkerConstraints constraints = 11;
}

これは実際のLLBでは下の部分に対応します。FROM golang:1.12の一行がこのようにOpに変換されていることがわかります。

{
  "Op": {
    "Op": {
      "Source": {
        "identifier": "docker-image://docker.io/library/golang:1.12"
      }
    },
    "platform": {
      "Architecture": "amd64",
      "OS": "darwin"
    },
    "constraints": {}
  },
  "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
  "OpMetadata": {
    "description": {
      "com.docker.dockerfile.v1.command": "FROM golang:1.12",
      "llb.customname": "[1/3] FROM docker.io/library/golang:1.12"
    },
    "caps": {
      "source.image": true
    }
  }
}

見るとわかる通り、Docker系の情報はOpMetadataの中にメタデータとして格納されるだけです。よってLLBがDockerfileの構造だけに依存している訳ではないことがわかります。

ここらでOpの種類を見てみましょう。Opの定義のoneof opを見るとわかる通り、実はOpの種類はわずか4種類しかありません。例えばExecOpは下記の部分です。

"Op": {
    "inputs": [
      // ...
    ],
    "Op": {
      "Exec": {
        "meta": {
          "args": [
            "/bin/sh",
            "-c",
            "go build -o go_bin"
          ],
          "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "GO111MODULE=on"
          ],
          "cwd": "/go"
        },
        "mounts": [
          {
            "input": 0,
            "dest": "/",
            "output": 0
          }
        ]
      }
    },
    "platform": {
      // ...
    },
    "constraints": {}
  },
  "Digest": "sha256:f2dd48bf7656705cfbefa478dca927bbe298d9adabd39576bfb7131ff57fd261",
  "OpMetadata": {
    // ...
  }

meta情報を見ると、argsにRUNで指定したコマンドが、cwdにWORKDIRの値が、envにENVで指定した環境変数がセットされています。実は、LLBの世界での環境変数ENVはExecOpだけに紐づいており、全てのOpに共通した設定というのができない構造になっています。

LLBが有向非循環構造になっているのを確認する

さてLLBの構成因子Opを確認したところでこれらが有向非循環グラフになっていることを確認しましょう。ポイントはdigestです。もう一度LLBを見てみましょう。

{
  "Op": {
    "Op": {
      // ...
  },
  "Digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5",
  "OpMetadata": {
    // ...
  }
}

Opには一つdigestがセットされています。一方でこのOpに依存するOpをみてみましょう.

{
  "Op": {
    "inputs": [
      {
        "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b",
        "index": 0
      },
      {
        "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67",
        "index": 0
      },
      {
        "digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5",
        "index": 0
      }
    ],
    "Op": {
      // ...
    },
    "platform": {
      // ...
    },
    "constraints": {}
  },
  "Digest": "sha256:d9fce5f43b1af2d2b9192de6eae55a2ea79a190c058cb9cfddb1af3ffbdb21d2",
  "OpMetadata": {
    // ...
    }
  }
}

inputsとしてエッジで紐付くOpのdigestの値が記載されています。このようにしてLLBはDAG構造を表現しています。例えば。下記のようなDockerfileは

FROM golang:1.12 AS stage0
WORKDIR /go
ADD ./ /go
RUN go build -o stage0_bin

FROM golang:1.12 AS stage1
WORKDIR /go
ADD ./ /go
RUN go build -o stage1_bin

FROM golang:1.12
COPY --from=stage0 /go/stage0_bin /
COPY --from=stage1 /go/stage1_bin /

digest値を追っていくと下記のようなDAG構造を取っていることがわかります。

なぜLLBを挟むことで並列化を実現できるかが一目でわかりますね。

まとめ

簡単にLLBの構造を追ってみて、なぜBuildKitでビルドの並列化ができるのかをみていきました。docker build の内部的な理解や、buildkikのコードリーディングをする際などに役立つはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DApps開発環境

DAppsの開発環境を設定していきます。

ツール

主に以下のものが必要になってきます

・Homebrew
Homebrewのインストールから操作まで

・Docker
Dockerインストールメモ

・Ganache
Windowsはこちらから
Macはこちらから

・Node.js

$ brew node install

・Truffle

$ brew install -g truffle//@4.1.1バージョン指定が必要な場合 

・Geth

$ brew install ethereum

ネットワークとは

Ethereumのブロックチェーンアプリを開発する際には、ネットワークに参加してブロックチェーンをしようしていく必要があります。

ブロックチェーンにおけるネットワークの仕組みを理解しておきたいと思います。

P2P(Peer to Peer)

Server-based-vs-P2P-network.jpg

今までは、サーバー型モデルと呼ばれる、サーバーがテータを保持し提供する仕組みが一般的でした。
ですがブロックチェーンではP2Pと呼ばれる、各ピア(それぞれのデバイスのこと、ノードともいう)がデータを保持し、互いにデータの提供、要求を行う自律分散型の仕組みで成り立っています。

ネットワークの種類

実際にブロックチェーンのアプリを開発する際には3種類のネットワークがあります。

メインネット

メインネットは実際の世界で使われているブロックチェーンです。
本番の環境ですので何かをやり取りする際には実際のETHが必要になります。
また、一度デプロイしてしまうとかき消すことはできません。

テストネット

メインネットにデプロイする前に開発したものがきちんとどうだするか確認するための「実験場」のようなものです。
ここでのトークンは市場価値がありません。

イーサリアムの代表的なテストネットは
・Rospoten
・Kovan
・Rinkeby

だだ、テストネットもネットに繋がっており攻撃を受けることがあり、どのテストネット一長一短あります。
Ethereum の各種テストネット比較

プライベートネット

構築者自身により構築されるネットワークのことで、個人や組織内で主に使います。

プライベートネットの構築

go-ethereumで構築する場合

Geth(Go Etherium)のプライベートネットワークの立ち上げ方法

規格のアップデートがあり仕様が変わってる部分があったなと感じたため
Gethを使ったプライベートネットの立ち上げはあまりお勧めではないです。

4.go-ethereumへの接続

$ geth attach http://localhost:8545

gethoを利用した場合

株式会社Popshoot提供する、プライベートネットをすぐに構築できるプラットフォームサービスを利用すれば簡単に構築することができます。
https://getho.io/

終わりに

ブロックチェーンにネットワークの仕組みな関してある程度理解できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Digdag+Embulkで異種RDB間のデータ転送をローカルで検証できるようにする

最近社内でDigdag+Embulkでデータ連携をする機会が増えてきたので、ローカルで検証できる環境を作りました。
開発用のサーバがあればそれを使っても良いですが、気軽に試せるローカル環境があると何かと便利です。

この記事ではDigdagEmbulkについては詳しく説明しないので、基本的な使い方を知っている方向けです。

試すこと

実際こんなことをすることはほとんど無いと思いますが
PostgreSQL -> SQLServer
のデータ転送をローカルだけでやってみます。

本当はクラウドで管理されているDWHに転送したり、S3に保存するケースが多いと思いますが今回はローカルで完結させることに主眼を置きます。

Embulkを使うだけだったら必ずしもDigdagは必要ありませんが、Embulkの処理は概ねスケジューリングされたバッチ処理で行うケースが多いと思うので、それを念頭に置いています。

実現させたい環境

下記の用な動作環境を作ります。

サービス名 環境
Digdag 0.9.33 ローカル
Embulk 0.9.5 コンテナ
Postgres 9.6 コンテナ
SQLServer 2017 コンテナ

Digdagの環境もDockerで構築できますが、Digdagのコンテナ上でEmbulkのコンテナを動作させることになり、Docker in Dockerな構成になるため避けています。

RDBのコンテナを立ち上げる

まずDBのコンテナを立ち上げます。
これは特に難しいことはないですが、下記の様なdocker-compose.ymlを作って立ち上げましょう。

version: "3"
services:
  postgres:
    image: postgres:9.6
    container_name: "postgres_practice"
    ports:
      - 5432:5432
    environment:
      - POSTGRES_USER=your_user
      - POSTGRES_PASSWORD=your_password
      - POSTGRES_DB=practice
    user: root

  sqlserver:
    image: microsoft/mssql-server-linux:2017-latest
    container_name: "sqlserver_practice"
    ports:
      - 1433:1433
    environment:
      ACCEPT_EULA: 'Y'
      SA_PASSWORD: 'YourP@ssword'

これでDBが立ち上がります。
ここには最低限の設定しか記載していないので、初期データロードやvolumeの永続化に関しては別途対応が必要になります。本記事では触れません。

Embulkが動作させるためのDockerfile

Embulkの動作環境を用意します。基本的なインストール方法は公式ドキュメント通りです。
Embulkにはいくつもプラグインが公開されおり、RDBのデータ入出力に関しては幅広く対応されています。
https://plugins.embulk.org/

FROM java:8-jre

RUN apt-get update && \
    apt-get -y install openssh-client

RUN curl --create-dirs -o /bin/embulk -L "https://dl.bintray.com/embulk/maven/embulk-0.9.5.jar" && \
    chmod +x /bin/embulk

RUN mkdir -p /your_embulk_bundle_dir
WORKDIR /your_embulk_bundle_dir

COPY Gemfile .
COPY Gemfile.lock .

RUN embulk bundle install --path vendor/bundle

今回はPostgreSQL -> SQLServerのデータ連携を実現することが目的なので
Gemfileは下記のように設定します。

source "http://rubygems.org"

gem 'embulk'
gem 'embulk-input-postgresql'
gem 'embulk-output-sqlserver'

inputとoutputでプラグインが分かれているので別で指定します。先頭のembulkも無いとエラーになるので入れます。

Gemfile.lockが無いとエラーになるので空ファイルを作ります。

touch Gemfile.lock

DigdagからDocker imageを使うときは既にimageがビルドされている必要があるため、適当な名前をつけてビルドします。

docker build -t practice_embulk . 

Digdagでワークフローを定義

ワークフローを定義するファイルは.digという拡張子のファイルを用いる必要があり、yamlベースの独自フォーマットで記述します。

practice.digというファイルを作成しておきます。

+practice:
  _export:
    docker:
      image: practice_embulk
  sh>: embulk run -b /your_embulk_bundle_dir rdb_data_transfer.yml

これは指定したコンテナ上でrdb_data_transfer.ymlに書かれた設定でemubulkを動作させるための命令を書いています。

_exportでdockerを指定すると、そのスコープ内で実行される命令をdocker run経由で実行してくれます。今回はEmbulkをDockerコンテナから使うので指定する必要があります。

次にembulk runコマンドで引数に与えているyamlファイルについて説明します。

Embulkの設定ファイルを作成

Embulkの設定ファイルはyaml形式で記述します。実際の運用ではデフォルトで使えるliquidというテンプレートエンジンが便利なのですが、この記事では必要無いので使いません。

rdb_data_transfer.ymlは下記のような設定にしておきます。

in:
  type: postgresql
  host: host.docker.internal
  user: your_user
  password: your_password
  database: practice
  query: |
    SELECT id,
           content
    FROM my_practice

out:
  type: sqlserver
  host: host.docker.internal
  user: sa
  password: YourP@ssword
  database: practice
  table: my_practice_from_postgres
  mode: truncate_insert

in: はデータの送信元からどんなクエリでデータを取得するかを記述します。今回は簡易的な例として my_practiceというテーブルのデータをquery:で指定したSQLを用いて全件取得しています。
詳しくは https://github.com/embulk/embulk-input-jdbc/tree/master/embulk-input-postgresql に書いてます。

out: はデータの挿入処理です。modeを指定することでデータの入れ方を変えることができます。今回はデータを一度truncateして、取得データをinsertする処理を試しました。このmodeではSQLServerにテーブルが存在しなければ作成してくれます。
詳しくは https://github.com/embulk/embulk-output-jdbc/tree/master/embulk-output-sqlserver に書いてます。

host:に指定しているhost.docker.internalはコンテナの中からホストを参照するために割り当てられるDockerコンテナ特有のDNS名です。

ホストの1433番と5432番ポートに対してリクエストを出すと、ポートフォワーディングによってコンテナに向けられるため結果的にDBにたどり着きます。
(この辺りの話は別途記事を作って解説しようと思います)

docker run時にオプションを指定するか、docker-composeを使ってコンテナに名前を割り当てたら簡単に名前解決できますが、この仕組みではどちらも実現できないためこのような方法を取っています。

実行

PostgreSQLにテーブルとテストデータを流し込みます。
コンテナを立ち上げる時に一緒に流し込むのが本当にはいいですが、少ないので手動でやります。
私はInteliJで接続して試しました。

CREATE TABLE my_practice (
    id int,
    content text
);

INSERT INTO my_practice (id, content) VALUES (1, 'practice1'), (2, 'practice2'), (3, 'practice3');

SQLServerでデータベースを作成します。

CREATE DATABASE practice COLLATE Japanese_CI_AS;

digdagコマンドで実行します。

digdag run practice.dig

実行して成功すると、下記のような表示になります。

2019-03-17 15:23:37.958 +0000: Embulk v0.9.5
2019-03-17 15:23:39.373 +0000 [INFO] (main): Started Embulk v0.9.5
2019-03-17 15:23:43.102 +0000 [INFO] (0001:transaction): BUNDLE_GEMFILE is being set: "/your_embulk_bundle_dir/Gemfile"
2019-03-17 15:23:43.103 +0000 [INFO] (0001:transaction): Gem's home and path are being cleared.
2019-03-17 15:23:45.928 +0000 [INFO] (0001:transaction): Loaded plugin embulk-input-postgresql (0.9.3)
2019-03-17 15:23:45.981 +0000 [INFO] (0001:transaction): Loaded plugin embulk-output-sqlserver (0.8.2)
2019-03-17 15:23:46.061 +0000 [INFO] (0001:transaction): JDBC Driver = /your_embulk_bundle_dir/vendor/bundle/jruby/2.3.0/gems/embulk-input-postgresql-0.9.3/default_jdbc_driver/postgresql-9.4-1205-jdbc41.jar
2019-03-17 15:23:46.069 +0000 [INFO] (0001:transaction): Connecting to jdbc:postgresql://host.docker.internal:5432/practice options {ApplicationName=embulk-input-postgresql, user=your_user, password=***, tcpKeepAlive=true, loginTimeout=300, socketTimeout=1800}
2019-03-17 15:23:46.177 +0000 [INFO] (0001:transaction): SQL: SET search_path TO "public"
2019-03-17 15:23:46.197 +0000 [INFO] (0001:transaction): Using JDBC Driver PostgreSQL 9.4 JDBC4.1 (build 1205)
2019-03-17 15:23:46.298 +0000 [INFO] (0001:transaction): Using local thread executor with max_threads=4 / output tasks 2 = input tasks 1 * 2
2019-03-17 15:23:46.350 +0000 [INFO] (0001:transaction): Using jTDS Driver
2019-03-17 15:23:46.355 +0000 [INFO] (0001:transaction): Connecting to jdbc:jtds:sqlserver://host.docker.internal:1433/practice options {user=sa, password=***}
2019-03-17 15:23:46.509 +0000 [INFO] (0001:transaction): Using JDBC Driver 1.3.1
2019-03-17 15:23:46.510 +0000 [INFO] (0001:transaction): Using truncate_insert mode
2019-03-17 15:23:46.592 +0000 [INFO] (0001:transaction): SQL: CREATE TABLE "my_practice_from_postgres_000001698c406e16_embulk000" ("id" BIGINT, "content" TEXT)
2019-03-17 15:23:46.601 +0000 [INFO] (0001:transaction): > 0.01 seconds
2019-03-17 15:23:46.610 +0000 [INFO] (0001:transaction): SQL: CREATE TABLE "my_practice_from_postgres_000001698c406e16_embulk001" ("id" BIGINT, "content" TEXT)
2019-03-17 15:23:46.620 +0000 [INFO] (0001:transaction): > 0.01 seconds
2019-03-17 15:23:46.856 +0000 [INFO] (0001:transaction): {done:  0 / 1, running: 0}
2019-03-17 15:23:46.920 +0000 [INFO] (0015:task-0000): Using jTDS Driver
2019-03-17 15:23:46.933 +0000 [INFO] (0015:task-0000): Connecting to jdbc:jtds:sqlserver://host.docker.internal:1433/practice options {user=sa, password=***}
2019-03-17 15:23:46.953 +0000 [INFO] (0015:task-0000): Prepared SQL: INSERT INTO "my_practice_from_postgres_000001698c406e16_embulk000" ("id", "content") VALUES (?, ?)
2019-03-17 15:23:46.963 +0000 [INFO] (0015:task-0000): Using jTDS Driver
2019-03-17 15:23:46.964 +0000 [INFO] (0015:task-0000): Connecting to jdbc:jtds:sqlserver://host.docker.internal:1433/practice options {user=sa, password=***}
2019-03-17 15:23:46.984 +0000 [INFO] (0015:task-0000): Prepared SQL: INSERT INTO "my_practice_from_postgres_000001698c406e16_embulk001" ("id", "content") VALUES (?, ?)
2019-03-17 15:23:47.064 +0000 [INFO] (0015:task-0000): Connecting to jdbc:postgresql://host.docker.internal:5432/practice options {ApplicationName=embulk-input-postgresql, user=your_user, password=***, tcpKeepAlive=true, loginTimeout=300, socketTimeout=1800}
2019-03-17 15:23:47.099 +0000 [INFO] (0015:task-0000): SQL: SET search_path TO "public"
2019-03-17 15:23:47.107 +0000 [INFO] (0015:task-0000): SQL: DECLARE cur NO SCROLL CURSOR FOR SELECT id,
       content
FROM my_practice

2019-03-17 15:23:47.114 +0000 [INFO] (0015:task-0000): SQL: FETCH FORWARD 10000 FROM cur
2019-03-17 15:23:47.121 +0000 [INFO] (0015:task-0000): > 0.00 seconds
2019-03-17 15:23:47.130 +0000 [INFO] (0015:task-0000): SQL: FETCH FORWARD 10000 FROM cur
2019-03-17 15:23:47.132 +0000 [INFO] (0015:task-0000): > 0.00 seconds
2019-03-17 15:23:47.135 +0000 [INFO] (0015:task-0000): Loading 3 rows
2019-03-17 15:23:47.159 +0000 [INFO] (0015:task-0000): > 0.02 seconds (loaded 3 rows in total)
2019-03-17 15:23:47.168 +0000 [INFO] (0001:transaction): {done:  1 / 1, running: 0}
2019-03-17 15:23:47.170 +0000 [INFO] (0001:transaction): Using jTDS Driver
2019-03-17 15:23:47.171 +0000 [INFO] (0001:transaction): Connecting to jdbc:jtds:sqlserver://host.docker.internal:1433/practice options {user=sa, password=***}
2019-03-17 15:23:47.213 +0000 [INFO] (0001:transaction): SQL: CREATE TABLE "my_practice_from_postgres" ("id" BIGINT, "content" TEXT)
2019-03-17 15:23:47.217 +0000 [INFO] (0001:transaction): > 0.00 seconds
2019-03-17 15:23:47.222 +0000 [INFO] (0001:transaction): SQL: DELETE FROM "my_practice_from_postgres"
2019-03-17 15:23:47.227 +0000 [INFO] (0001:transaction): > 0.00 seconds
2019-03-17 15:23:47.227 +0000 [INFO] (0001:transaction): SQL: INSERT INTO "my_practice_from_postgres" ("id", "content") SELECT "id", "content" FROM "my_practice_from_postgres_000001698c406e16_embulk000" UNION ALL SELECT "id", "content" FROM "my_practice_from_postgres_000001698c406e16_embulk001"
2019-03-17 15:23:47.235 +0000 [INFO] (0001:transaction): > 0.01 seconds (3 rows)
2019-03-17 15:23:47.298 +0000 [INFO] (0001:transaction): Using jTDS Driver
2019-03-17 15:23:47.299 +0000 [INFO] (0001:transaction): Connecting to jdbc:jtds:sqlserver://host.docker.internal:1433/practice options {user=sa, password=***}
2019-03-17 15:23:47.331 +0000 [INFO] (0001:transaction): SQL: DROP TABLE "my_practice_from_postgres_000001698c406e16_embulk000"
2019-03-17 15:23:47.343 +0000 [INFO] (0001:transaction): > 0.01 seconds
2019-03-17 15:23:47.355 +0000 [INFO] (0001:transaction): SQL: DROP TABLE "my_practice_from_postgres_000001698c406e16_embulk001"
2019-03-17 15:23:47.371 +0000 [INFO] (0001:transaction): > 0.02 seconds
2019-03-17 15:23:47.387 +0000 [INFO] (main): Committed.

これでローカルだけで、PostgreSQLからSQLServerへのデータ転送処理が実現できました。
実際にSQLServer上にテーブルが作成されてデータが挿入されていることが確認できるはずです。

最後に

簡易な設定だけで実現できるのでコンテナの技術とEmbulkの利便性を感じました。
Embulkのプラグインは機能が豊富でいろんなパターンに対応可能なので、色々試すと面白いです。

今回はとてもシンプルな例だったためEmbulkの設定ファイルは簡潔でしたが、実際の業務で必要なテーブルになるとデータの形式によってはfilterを入れる必要があったりするため、色々と考えることはあります。
あくまで導入の第一歩としての紹介でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Windows】Docker Desktop for WindowsとVagrant+Hyper-V環境を共存させる #2

※本稿はこの記事の続きです。
【Windows】Docker Desktop for WindowsとVagrant+Hyper-V環境を共存させる #1 - Qiita

手順

Hyper-V通常VM用のネットワーク(Hyper-V Internal with WinNAT)を設定する

次に、通常のVM用のネットワークを設定していきます。大きく以下の3ステップの作業となります。

  • 新たにVirtual Switchを作成する
  • 当該Virtual Switchに接続するホストNICにIPアドレスを設定する
  • WinNATを利用し、当該Virtual Switchから外部のネットワークへの通信をNATするよう設定する

基本的には下記Microsoftの公式ドキュメントを通りの手順となります。

Set up a NAT network | Microsoft Docs
https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/setup-nat-network

WinNATに関する、より具体的な説明、制約事項等は下記記事にまとまっています。併せて読むとWinNATに関する理解が深まります。

Windows NAT (WinNAT) — Capabilities and limitations | Virtualization Blog
https://blogs.technet.microsoft.com/virtualization/2016/05/25/windows-nat-winnat-capabilities-and-limitations/

PowerShellをAdministrator権限で実行する

スタートメニューから「powershell」と入力すると、Power Shellのアイコンが表示されます。「Run As Administrator」をクリックします。
PowerShell_Administrator_01.png

その後、User Access Controlのポップアップが表示されるので、対象が間違っていないことを確認し、「Yes」を押します。
Power ShellがAdministrator権限で起動します。タイトルバーの左上に「Administrator:」と表示されていることを確認します。
PowerShell_Administrator_02.png

新たにVirtual Switchを作成する(New-VMSwitch)

PowerShellから下記のコマンドを実行します。Virtual Switchの名前は何でもよいですが、ここでは「Hyper-V Internal with WinNAT」とします。Virtual Switchの作成には数分かかります。

  • SwitchName: Hyper-V Internal with WinNAT (※任意に設定可能)
  • SwitchType: Internal
New-VMSwitch
PS C:\WINDOWS\system32> New-VMSwitch -SwitchName "Hyper-V Internal with WinNAT" -SwitchType Internal

Name                         SwitchType NetAdapterInterfaceDescription
----                         ---------- ------------------------------
Hyper-V Internal with WinNAT Internal

上記コマンドが完了したら、Virtual Switchが生成されているかどうかを確認します。

Get-VMSwitch
PS C:\WINDOWS\system32> Get-VMSwitch

Name                         SwitchType NetAdapterInterfaceDescription
----                         ---------- ------------------------------
DockerNAT                    Internal
Hyper-V Internal with WinNAT Internal

念のため、Hyper-V ManagerからVirtual Switchを確認してみます。Hyper-V Managerを開き、右の「Actions」ペインから「Virtual Switch Manager...」をクリックします。
「Hyper-V Internal with WinNAT」というVirtual Switchが生成されていることが確認できます。
WinNATConfiguration_01.png

当該Virtual Switchに接続するホストNICにIPアドレスを設定する

先ほどの当該Virtual Switchの作成に伴い、これに接続されるホストNICも併せて生成されています。Get-NetAdapterコマンドレットで、存在するNICの一覧を確認できますので、確認しておきます。
下記例では、「ifIndex」が「32」のものが該当します。この「ifIndex」の値は次に使うので控えておきます。

Get-NetAdapter
PS C:\WINDOWS\system32> Get-NetAdapter

Name                      InterfaceDescription                    ifIndex Status       MacAddress             LinkSpeed
----                      --------------------                    ------- ------       ----------             ---------
vEthernet (Hyper-V Int... Hyper-V Virtual Ethernet Adapter #2          32 Up           00-15-5D-14-25-10        10 Gbps
ローカル エリア接続       Realtek PCIe GBE Family Controller            9 Up           80-EE-73-2E-ED-3E         1 Gbps
vEthernet (DockerNAT)     Hyper-V Virtual Ethernet Adapter             26 Up           00-15-5D-14-25-0E        10 Gbps

NICにIPアドレスを付与するには、New-NetIPAddressというコマンドレットを利用します。対象のNICを指定するにあたり「ifIndex」の値を指定します。
なお、ここで設定するIPアドレスは、Hyper-V内のVMから見た場合にDefault Gatewayとなります。

  • IPAddress: 192.168.254.1
  • PrefixLength: 23
  • InterfaceIndex: 32 (※Get-NetAdapterコマンドレットであらかじめ確認する)
New-NetIPAddress
PS C:\WINDOWS\system32> New-NetIPAddress -IPAddress 192.168.254.1 -PrefixLength 23 -InterfaceIndex 32


IPAddress         : 192.168.254.1
InterfaceIndex    : 32
InterfaceAlias    : vEthernet (Hyper-V Internal with WinNAT)
AddressFamily     : IPv4
Type              : Unicast
PrefixLength      : 23
PrefixOrigin      : Manual
SuffixOrigin      : Manual
AddressState      : Tentative
ValidLifetime     : Infinite ([TimeSpan]::MaxValue)
PreferredLifetime : Infinite ([TimeSpan]::MaxValue)
SkipAsSource      : False
PolicyStore       : ActiveStore

IPAddress         : 192.168.254.1
InterfaceIndex    : 32
InterfaceAlias    : vEthernet (Hyper-V Internal with WinNAT)
AddressFamily     : IPv4
Type              : Unicast
PrefixLength      : 23
PrefixOrigin      : Manual
SuffixOrigin      : Manual
AddressState      : Invalid
ValidLifetime     : Infinite ([TimeSpan]::MaxValue)
PreferredLifetime : Infinite ([TimeSpan]::MaxValue)
SkipAsSource      : False
PolicyStore       : PersistentStore

なお、ここで設定したIPアドレスは、「Network and Sharing Center」からGUIで設定するものと同一です。「Control Panel\Network and Internet\Network Connections」から対象のNICのPropertiesを開き、IPv4の設定を確認してみましょう。

WinNATConfiguration_06.png
WinNATConfiguration_07.png

WinNATを利用し、当該Virtual Switchから外部のネットワークへの通信をNATするよう設定する

最後に、今回作成したVirtual Switchに所属する(正確には、指定したIPアドレスレンジに所属する)ホストからのトラフィックをNATするための設定を追加します。

PowerShellからNew-NetNatコマンドレットを実行します。パラメーターは下記を指定しました。

  • Name: HyperVinternalWinNAT (※WinNAT設定に対する名称。任意のものを指定可能。)
  • InternalIPInterfaceAddressPrefix: NAT対象となるIPアドレスレンジ
New-NetNat
PS C:\WINDOWS\system32> New-NetNat -Name HyperVinternalWinNAT -InternalIPInterfaceAddressPrefix 192.168.254.0/23


Name                             : HyperVinternalWinNAT
ExternalIPInterfaceAddressPrefix :
InternalIPInterfaceAddressPrefix : 192.168.254.0/23
IcmpQueryTimeout                 : 30
TcpEstablishedConnectionTimeout  : 1800
TcpTransientConnectionTimeout    : 120
TcpFilteringBehavior             : AddressDependentFiltering
UdpFilteringBehavior             : AddressDependentFiltering
UdpIdleSessionTimeout            : 120
UdpInboundRefresh                : False
Store                            : Local
Active                           : True

以上でネットワークの設定は完了です。

Hyper-V通常VM用のネットワーク(Hyper-V Internal with WinNAT)からの疎通を確認する

VagrantのProviderとしてHyper-Vを利用する場合の注意事項

Vagrant BoxはProvider依存

Vagrant BoxはProviderが違う場合はイメージを利用することができません。そのため、Hyper-V用のBoxを探す必要があります。

Basic Usage - Providers - Vagrant by HashiCorp
https://www.vagrantup.com/docs/providers/basic_usage.html

Basic Provider Usage
» Boxes
Vagrant boxes are all provider-specific. A box for VirtualBox is incompatible with the VMware Fusion provider, or any other provider. A box must be installed for each provider, and can share the same name as other boxes as long as the providers differ. So you can have both a VirtualBox and VMware Fusion "precise64" box.

VagrantはHyper-Vの仮想ネットワークを操作しない

ProviderとしてHyper-Vを利用する場合、VagrantはVirtual Switchなどのネットワークに関する設定を何も行いません。設定済みのものを利用するよう動作します。

Limitations - Hyper-V Provider - Vagrant by HashiCorp
https://www.vagrantup.com/docs/hyperv/limitations.html

Limited Networking
Vagrant does not yet know how to create and configure new networks for Hyper-V. When launching a machine with Hyper-V, Vagrant will prompt you asking what virtual switch you want to connect the virtual machine to.

A result of this is that networking configurations in the Vagrantfile are completely ignored with Hyper-V. Vagrant cannot enforce a static IP or automatically configure a NAT.

However, the IP address of the machine will be reported as part of the vagrant up, and you can use that IP address as if it were a host only network.

Vagrantから新たなインスタンスを起動する

今回はAmazon Linux 2のVMを起動したいと思います。
Hyper-V用のAmazon Linux 2というニッチ需要に対応するものがあるかなと思ったら、作ってくれていた方がいました。今回はありがたくこれを利用します。

Vagrant box Yojimbo108/AmazonLinux2 - Vagrant Cloud
https://app.vagrantup.com/Yojimbo108/boxes/AmazonLinux2

Vagrantfileの設定はまずはシンプルに下記のようにします。ホスト名は「stdsv3」とします。

Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "Yojimbo108/AmazonLinux2"

  config.vm.provider "hyperv" do |v|
    v.memory = 1024
    v.cpus = 1
  end

  config.vm.define :"stdsv3" do |c1|
    c1.vm.hostname = "stdsv3"
  end

end

Administrator権限でGitBashを起動する

Vagrantfileができたら、vagrant upを実行していきます。

その前に、Hyper-VをProviderに利用する場合、vagrantコマンドの実行にはAdministrator権限が必要になります。

まず、先ほどのPowerShellと同様の手順で、GitBashをAdministrator権限で実行します。
Context Menuに追加する方法については下記記事にまとめましたので併せて参考にしてください。

【Windows】GitBashをcontext menuからAdministrator権限付きで実行する - Qiita

vagrant upを実行する

先ほど作成したVagrantfileのあるフォルダで、Administrator権限でGitBashを実行し、vagrant upを実行します。
Administrator権限でないと実行できませんので注意してください。

実行中、以下の2つについて聞かれますので、適宜入力します。

  • どのVirtual Switchを利用するか
  • SMBによるファイル共有用のユーザ名・パスワード

2つ以上のVirtual Switchが存在する場合、vagrant up実行後、下記のようにどのVirtual Switchを利用するか聞かれます。名称はこれまでの手順で作成したものが表示されています。
ここでは「2」を指定します。

vagrant up
$ vagrant up
Bringing machine 'stdsv3' up with 'hyperv' provider...
==> stdsv3: Verifying Hyper-V is enabled...
==> stdsv3: Verifying Hyper-V is accessible...
==> stdsv3: Importing a Hyper-V instance
    stdsv3: Creating and registering the VM...
    stdsv3: Successfully imported VM
    stdsv3: Please choose a switch to attach to your Hyper-V instance.
    stdsv3: If none of these are appropriate, please open the Hyper-V manager
    stdsv3: to create a new virtual switch.
    stdsv3:
    stdsv3: 1) DockerNAT
    stdsv3: 2) Hyper-V Internal with WinNAT
    stdsv3:
    stdsv3: What switch would you like to use?

Virtual Switchを指定すると、処理を継続したのち、SMB用のユーザ名・パスワードを聞かれます。ProviderとしてHyper-Vを利用する場合、shared_folder用のプロトコルとしてSMBを利用するので、この接続用のアカウントとなります。
自分の作業用のアカウントで良いでしょう。

vagrant up(続き)
$ vagrant up
Bringing machine 'stdsv3' up with 'hyperv' provider...
==> stdsv3: Verifying Hyper-V is enabled...
==> stdsv3: Verifying Hyper-V is accessible...
==> stdsv3: Importing a Hyper-V instance
    stdsv3: Creating and registering the VM...
    stdsv3: Successfully imported VM
    stdsv3: Please choose a switch to attach to your Hyper-V instance.
    stdsv3: If none of these are appropriate, please open the Hyper-V manager
    stdsv3: to create a new virtual switch.
    stdsv3:
    stdsv3: 1) DockerNAT
    stdsv3: 2) Hyper-V Internal with WinNAT
    stdsv3:
    stdsv3: What switch would you like to use? 2
    stdsv3: Configuring the VM...
==> stdsv3: Starting the machine...
==> stdsv3: Waiting for the machine to report its IP address...
    stdsv3: Timeout: 120 seconds
    stdsv3: IP: fe80::215:5dff:fe14:2511
==> stdsv3: Waiting for machine to boot. This may take a few minutes...
    stdsv3: SSH address: fe80::215:5dff:fe14:2511:22
    stdsv3: SSH username: vagrant
    stdsv3: SSH auth method: private key
    stdsv3: Warning: Remote connection disconnect. Retrying...
    stdsv3:
    stdsv3: Vagrant insecure key detected. Vagrant will automatically replace
    stdsv3: this with a newly generated keypair for better security.
    stdsv3:
    stdsv3: Inserting generated public key within guest...
    stdsv3: Removing insecure key from the guest if it's present...
    stdsv3: Key inserted! Disconnecting and reconnecting using new SSH key...
==> stdsv3: Machine booted and ready!
==> stdsv3: Preparing SMB shared folders...
    stdsv3: You will be asked for the username and password to use for the SMB
    stdsv3: folders shortly. Please use the proper username/password of your
    stdsv3: account.
    stdsv3:
    stdsv3: Username: ****
    stdsv3: Password (will be hidden):
Error! Your console doesn't support hiding input. We'll ask for
input again below, but we WILL NOT be able to hide input. If this
is a problem for you, ctrl-C to exit and fix your stdin.
     stdsv3: Password (will be hidden): ********

Vagrant requires administrator access to create SMB shares and
may request access to complete setup of configured shares.
==> stdsv3: Setting hostname...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

service network restart

Stdout from the command:

Restarting network (via systemctl):  [FAILED]


Stderr from the command:

Job for network.service failed because the control process exited with error code. See "systemctl status network.service" and "journalctl -xe" for details.

最終的に、Amazon Linux 2内のnetwork設定でFAILEDとなります。おそらく、「Hyper-V Internal with WinNAT」ネットワーク内でDHCPサーバが稼働していないため、IPv4の設定に失敗したのでしょう。

ちなみに、IPv4の設定は失敗していますが、IPv6の設定は行われているので、「vagrant ssh」することが可能です。(IPv6経由での接続になります。)
「vagrant ssh」でVMにログインしたのち、手動でIPv4の設定を行い、ネットワークの疎通を確認していきます。

VM内のネットワーク設定を手動でおこなう

まずは、今回作成したVMにログインします。vagrant sshを実行します。

vagrant ssh
$ vagrant ssh
Last login: Fri Mar  1 15:21:31 2019

   __|  __|_  )
   _|  (     /   Amazon Linux 2 AMI
  ___|\___|___|

https://aws.amazon.com/amazon-linux-2/

ifconfigコマンドを実行し、現在のネットワーク設定を見てみます。IPv6の設定はあるものの、IPv4の設定がありません。

ifconfig
[vagrant@stdsv3 ~]$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
    inet6 fe80::215:5dff:fe14:2511  prefixlen 64  scopeid 0x20<link>
    ether 00:15:5d:14:25:11  txqueuelen 1000  (Ethernet)
    RX packets 5142  bytes 1149687 (1.0 MiB)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 474  bytes 92644 (90.4 KiB)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
    inet 127.0.0.1  netmask 255.0.0.0
    inet6 ::1  prefixlen 128  scopeid 0x10<host>
    loop  txqueuelen 1000  (Local Loopback)
    RX packets 0  bytes 0 (0.0 B)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 0  bytes 0 (0.0 B)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

手動でIPアドレスを設定します。今回は「192.168.254.13/23」を設定します。設定後、ifconfigコマンドを実行し、意図したとおりにIPv4アドレスが付与されたことを確認します。

ip address add
[vagrant@stdsv3 ~]$ sudo ip address add 192.168.254.13/23 dev eth0

[vagrant@stdsv3 ~]$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.254.13  netmask 255.255.254.0  broadcast 0.0.0.0
        inet6 fe80::215:5dff:fe14:2511  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:14:25:11  txqueuelen 1000  (Ethernet)
        RX packets 5395  bytes 1185026 (1.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 617  bytes 110178 (107.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

「Hyper-V Internal with WinNAT」ネットワーク用のDefault Gatewayである「192.168.254.1」との通信ができるかどうか確認します。pingの応答はないものの、ARPの解決ができているので、L2レベルで通信できることは確認できました。
おそらく、当該ネットワークがPublic Networkになっているため、ホストOS側がICMPパケットを遮断しているものと推測できます。

ping
[vagrant@stdsv3 ~]$ ping 192.168.254.1
PING 192.168.254.1 (192.168.254.1) 56(84) bytes of data.
^C
--- 192.168.254.1 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1027ms

[vagrant@stdsv3 ~]$ arp -a
? (192.168.254.1) at 00:15:5d:14:25:10 [ether] on eth0

「192.168.254.1」をデフォルトゲートウェイとして追加します。

route add
[vagrant@stdsv3 ~]$ sudo route add default gw 192.168.254.1

[vagrant@stdsv3 ~]$ netstat -arn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.254.1   0.0.0.0         UG        0 0          0 eth0
192.168.254.0   0.0.0.0         255.255.254.0   U         0 0          0 eth0

Internet上の適当なIPアドレス(今回は8.8.8.8を利用)にpingを実行します。対象のIPアドレスからping応答が返ってくることが確認できました。

ping
[vagrant@stdsv3 ~]$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=14.8 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=11.8 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 11.853/13.340/14.827/1.487 ms

以上で、VM内からInternetに対して通信できることが確認できました。

VM内の設定を毎回手動でおこなうわけにもいきませんので、これを自動的に設定する方法を考えます。大きく以下の2通りの方法で実現することとなります。

  • VagrantによるVMインスタンス構築時、IPv4設定を併せて行う
  • ホストOS側でDHCPサーバを稼働させ、「Hyper-V Internal with WinNAT」ネットワークに所属するゲストOSがDHCPでIPv4設定できるようにする

Hyper-V通常VM用のネットワーク設定を自動化する

※続きます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Windows】Docker Desktop for WindowsとVagrant+Hyper-V環境を共存させる #2/3

※本稿はこの記事の続きです。
【Windows】Docker Desktop for WindowsとVagrant+Hyper-V環境を共存させる #1/3 - Qiita

手順

Hyper-V通常VM用のネットワーク(Hyper-V Internal with WinNAT)を設定する

次に、通常のVM用のネットワークを設定していきます。大きく以下の3ステップの作業となります。

  • 新たにVirtual Switchを作成する
  • 当該Virtual Switchに接続するホストNICにIPアドレスを設定する
  • WinNATを利用し、当該Virtual Switchから外部のネットワークへの通信をNATするよう設定する

基本的には下記Microsoftの公式ドキュメントを通りの手順となります。

Set up a NAT network | Microsoft Docs
https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/setup-nat-network

WinNATに関する、より具体的な説明、制約事項等は下記記事にまとまっています。併せて読むとWinNATに関する理解が深まります。

Windows NAT (WinNAT) — Capabilities and limitations | Virtualization Blog
https://blogs.technet.microsoft.com/virtualization/2016/05/25/windows-nat-winnat-capabilities-and-limitations/

PowerShellをAdministrator権限で実行する

スタートメニューから「powershell」と入力すると、Power Shellのアイコンが表示されます。「Run As Administrator」をクリックします。
PowerShell_Administrator_01.png

その後、User Access Controlのポップアップが表示されるので、対象が間違っていないことを確認し、「Yes」を押します。
Power ShellがAdministrator権限で起動します。タイトルバーの左上に「Administrator:」と表示されていることを確認します。
PowerShell_Administrator_02.png

新たにVirtual Switchを作成する(New-VMSwitch)

PowerShellから下記のコマンドを実行します。Virtual Switchの名前は何でもよいですが、ここでは「Hyper-V Internal with WinNAT」とします。Virtual Switchの作成には数分かかります。

  • SwitchName: Hyper-V Internal with WinNAT (※任意に設定可能)
  • SwitchType: Internal
New-VMSwitch
PS C:\WINDOWS\system32> New-VMSwitch -SwitchName "Hyper-V Internal with WinNAT" -SwitchType Internal

Name                         SwitchType NetAdapterInterfaceDescription
----                         ---------- ------------------------------
Hyper-V Internal with WinNAT Internal

上記コマンドが完了したら、Virtual Switchが生成されているかどうかを確認します。

Get-VMSwitch
PS C:\WINDOWS\system32> Get-VMSwitch

Name                         SwitchType NetAdapterInterfaceDescription
----                         ---------- ------------------------------
DockerNAT                    Internal
Hyper-V Internal with WinNAT Internal

念のため、Hyper-V ManagerからVirtual Switchを確認してみます。Hyper-V Managerを開き、右の「Actions」ペインから「Virtual Switch Manager...」をクリックします。
「Hyper-V Internal with WinNAT」というVirtual Switchが生成されていることが確認できます。
WinNATConfiguration_01.png

当該Virtual Switchに接続するホストNICにIPアドレスを設定する

先ほどの当該Virtual Switchの作成に伴い、これに接続されるホストNICも併せて生成されています。Get-NetAdapterコマンドレットで、存在するNICの一覧を確認できますので、確認しておきます。
下記例では、「ifIndex」が「32」のものが該当します。この「ifIndex」の値は次に使うので控えておきます。

Get-NetAdapter
PS C:\WINDOWS\system32> Get-NetAdapter

Name                      InterfaceDescription                    ifIndex Status       MacAddress             LinkSpeed
----                      --------------------                    ------- ------       ----------             ---------
vEthernet (Hyper-V Int... Hyper-V Virtual Ethernet Adapter #2          32 Up           00-15-5D-14-25-10        10 Gbps
ローカル エリア接続       Realtek PCIe GBE Family Controller            9 Up           80-EE-73-2E-ED-3E         1 Gbps
vEthernet (DockerNAT)     Hyper-V Virtual Ethernet Adapter             26 Up           00-15-5D-14-25-0E        10 Gbps

NICにIPアドレスを付与するには、New-NetIPAddressというコマンドレットを利用します。対象のNICを指定するにあたり「ifIndex」の値を指定します。
なお、ここで設定するIPアドレスは、Hyper-V内のVMから見た場合にDefault Gatewayとなります。

  • IPAddress: 192.168.254.1
  • PrefixLength: 23
  • InterfaceIndex: 32 (※Get-NetAdapterコマンドレットであらかじめ確認する)
New-NetIPAddress
PS C:\WINDOWS\system32> New-NetIPAddress -IPAddress 192.168.254.1 -PrefixLength 23 -InterfaceIndex 32


IPAddress         : 192.168.254.1
InterfaceIndex    : 32
InterfaceAlias    : vEthernet (Hyper-V Internal with WinNAT)
AddressFamily     : IPv4
Type              : Unicast
PrefixLength      : 23
PrefixOrigin      : Manual
SuffixOrigin      : Manual
AddressState      : Tentative
ValidLifetime     : Infinite ([TimeSpan]::MaxValue)
PreferredLifetime : Infinite ([TimeSpan]::MaxValue)
SkipAsSource      : False
PolicyStore       : ActiveStore

IPAddress         : 192.168.254.1
InterfaceIndex    : 32
InterfaceAlias    : vEthernet (Hyper-V Internal with WinNAT)
AddressFamily     : IPv4
Type              : Unicast
PrefixLength      : 23
PrefixOrigin      : Manual
SuffixOrigin      : Manual
AddressState      : Invalid
ValidLifetime     : Infinite ([TimeSpan]::MaxValue)
PreferredLifetime : Infinite ([TimeSpan]::MaxValue)
SkipAsSource      : False
PolicyStore       : PersistentStore

なお、ここで設定したIPアドレスは、「Network and Sharing Center」からGUIで設定するものと同一です。「Control Panel\Network and Internet\Network Connections」から対象のNICのPropertiesを開き、IPv4の設定を確認してみましょう。

WinNATConfiguration_06.png
WinNATConfiguration_07.png

WinNATを利用し、当該Virtual Switchから外部のネットワークへの通信をNATするよう設定する

最後に、今回作成したVirtual Switchに所属する(正確には、指定したIPアドレスレンジに所属する)ホストからのトラフィックをNATするための設定を追加します。

PowerShellからNew-NetNatコマンドレットを実行します。パラメーターは下記を指定しました。

  • Name: HyperVinternalWinNAT (※WinNAT設定に対する名称。任意のものを指定可能。)
  • InternalIPInterfaceAddressPrefix: NAT対象となるIPアドレスレンジ
New-NetNat
PS C:\WINDOWS\system32> New-NetNat -Name HyperVinternalWinNAT -InternalIPInterfaceAddressPrefix 192.168.254.0/23


Name                             : HyperVinternalWinNAT
ExternalIPInterfaceAddressPrefix :
InternalIPInterfaceAddressPrefix : 192.168.254.0/23
IcmpQueryTimeout                 : 30
TcpEstablishedConnectionTimeout  : 1800
TcpTransientConnectionTimeout    : 120
TcpFilteringBehavior             : AddressDependentFiltering
UdpFilteringBehavior             : AddressDependentFiltering
UdpIdleSessionTimeout            : 120
UdpInboundRefresh                : False
Store                            : Local
Active                           : True

以上でネットワークの設定は完了です。

Hyper-V通常VM用のネットワーク(Hyper-V Internal with WinNAT)からの疎通を確認する

VagrantのProviderとしてHyper-Vを利用する場合の注意事項

Vagrant BoxはProvider依存

Vagrant BoxはProviderが違う場合はイメージを利用することができません。そのため、Hyper-V用のBoxを探す必要があります。

Basic Usage - Providers - Vagrant by HashiCorp
https://www.vagrantup.com/docs/providers/basic_usage.html

Basic Provider Usage
» Boxes
Vagrant boxes are all provider-specific. A box for VirtualBox is incompatible with the VMware Fusion provider, or any other provider. A box must be installed for each provider, and can share the same name as other boxes as long as the providers differ. So you can have both a VirtualBox and VMware Fusion "precise64" box.

VagrantはHyper-Vの仮想ネットワークを操作しない

ProviderとしてHyper-Vを利用する場合、VagrantはVirtual Switchなどのネットワークに関する設定を何も行いません。設定済みのものを利用するよう動作します。

Limitations - Hyper-V Provider - Vagrant by HashiCorp
https://www.vagrantup.com/docs/hyperv/limitations.html

Limited Networking
Vagrant does not yet know how to create and configure new networks for Hyper-V. When launching a machine with Hyper-V, Vagrant will prompt you asking what virtual switch you want to connect the virtual machine to.

A result of this is that networking configurations in the Vagrantfile are completely ignored with Hyper-V. Vagrant cannot enforce a static IP or automatically configure a NAT.

However, the IP address of the machine will be reported as part of the vagrant up, and you can use that IP address as if it were a host only network.

Vagrantから新たなインスタンスを起動する

今回はAmazon Linux 2のVMを起動したいと思います。
Hyper-V用のAmazon Linux 2というニッチ需要に対応するものがあるかなと思ったら、作ってくれていた方がいました。今回はありがたくこれを利用します。

Vagrant box Yojimbo108/AmazonLinux2 - Vagrant Cloud
https://app.vagrantup.com/Yojimbo108/boxes/AmazonLinux2

Vagrantfileの設定はまずはシンプルに下記のようにします。ホスト名は「stdsv3」とします。

Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "Yojimbo108/AmazonLinux2"

  config.vm.provider "hyperv" do |v|
    v.memory = 1024
    v.cpus = 1
  end

  config.vm.define :"stdsv3" do |c1|
    c1.vm.hostname = "stdsv3"
  end

end

Administrator権限でGitBashを起動する

Vagrantfileができたら、vagrant upを実行していきます。

その前に、Hyper-VをProviderに利用する場合、vagrantコマンドの実行にはAdministrator権限が必要になります。

まず、先ほどのPowerShellと同様の手順で、GitBashをAdministrator権限で実行します。
Context Menuに追加する方法については下記記事にまとめましたので併せて参考にしてください。

【Windows】GitBashをcontext menuからAdministrator権限付きで実行する - Qiita

vagrant upを実行する

先ほど作成したVagrantfileのあるフォルダで、Administrator権限でGitBashを実行し、vagrant upを実行します。
Administrator権限でないと実行できませんので注意してください。

実行中、以下の2つについて聞かれますので、適宜入力します。

  • どのVirtual Switchを利用するか
  • SMBによるファイル共有用のユーザ名・パスワード

2つ以上のVirtual Switchが存在する場合、vagrant up実行後、下記のようにどのVirtual Switchを利用するか聞かれます。名称はこれまでの手順で作成したものが表示されています。
ここでは「2」を指定します。

vagrant up
$ vagrant up
Bringing machine 'stdsv3' up with 'hyperv' provider...
==> stdsv3: Verifying Hyper-V is enabled...
==> stdsv3: Verifying Hyper-V is accessible...
==> stdsv3: Importing a Hyper-V instance
    stdsv3: Creating and registering the VM...
    stdsv3: Successfully imported VM
    stdsv3: Please choose a switch to attach to your Hyper-V instance.
    stdsv3: If none of these are appropriate, please open the Hyper-V manager
    stdsv3: to create a new virtual switch.
    stdsv3:
    stdsv3: 1) DockerNAT
    stdsv3: 2) Hyper-V Internal with WinNAT
    stdsv3:
    stdsv3: What switch would you like to use?

Virtual Switchを指定すると、処理を継続したのち、SMB用のユーザ名・パスワードを聞かれます。ProviderとしてHyper-Vを利用する場合、shared_folder用のプロトコルとしてSMBを利用するので、この接続用のアカウントとなります。
自分の作業用のアカウントで良いでしょう。

vagrant up(続き)
$ vagrant up
Bringing machine 'stdsv3' up with 'hyperv' provider...
==> stdsv3: Verifying Hyper-V is enabled...
==> stdsv3: Verifying Hyper-V is accessible...
==> stdsv3: Importing a Hyper-V instance
    stdsv3: Creating and registering the VM...
    stdsv3: Successfully imported VM
    stdsv3: Please choose a switch to attach to your Hyper-V instance.
    stdsv3: If none of these are appropriate, please open the Hyper-V manager
    stdsv3: to create a new virtual switch.
    stdsv3:
    stdsv3: 1) DockerNAT
    stdsv3: 2) Hyper-V Internal with WinNAT
    stdsv3:
    stdsv3: What switch would you like to use? 2
    stdsv3: Configuring the VM...
==> stdsv3: Starting the machine...
==> stdsv3: Waiting for the machine to report its IP address...
    stdsv3: Timeout: 120 seconds
    stdsv3: IP: fe80::215:5dff:fe14:2511
==> stdsv3: Waiting for machine to boot. This may take a few minutes...
    stdsv3: SSH address: fe80::215:5dff:fe14:2511:22
    stdsv3: SSH username: vagrant
    stdsv3: SSH auth method: private key
    stdsv3: Warning: Remote connection disconnect. Retrying...
    stdsv3:
    stdsv3: Vagrant insecure key detected. Vagrant will automatically replace
    stdsv3: this with a newly generated keypair for better security.
    stdsv3:
    stdsv3: Inserting generated public key within guest...
    stdsv3: Removing insecure key from the guest if it's present...
    stdsv3: Key inserted! Disconnecting and reconnecting using new SSH key...
==> stdsv3: Machine booted and ready!
==> stdsv3: Preparing SMB shared folders...
    stdsv3: You will be asked for the username and password to use for the SMB
    stdsv3: folders shortly. Please use the proper username/password of your
    stdsv3: account.
    stdsv3:
    stdsv3: Username: ****
    stdsv3: Password (will be hidden):
Error! Your console doesn't support hiding input. We'll ask for
input again below, but we WILL NOT be able to hide input. If this
is a problem for you, ctrl-C to exit and fix your stdin.
     stdsv3: Password (will be hidden): ********

Vagrant requires administrator access to create SMB shares and
may request access to complete setup of configured shares.
==> stdsv3: Setting hostname...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

service network restart

Stdout from the command:

Restarting network (via systemctl):  [FAILED]


Stderr from the command:

Job for network.service failed because the control process exited with error code. See "systemctl status network.service" and "journalctl -xe" for details.

最終的に、Amazon Linux 2内のnetwork設定でFAILEDとなります。おそらく、「Hyper-V Internal with WinNAT」ネットワーク内でDHCPサーバが稼働していないため、IPv4の設定に失敗したのでしょう。

ちなみに、IPv4の設定は失敗していますが、IPv6の設定は行われているので、「vagrant ssh」することが可能です。(IPv6経由での接続になります。)
「vagrant ssh」でVMにログインしたのち、手動でIPv4の設定を行い、ネットワークの疎通を確認していきます。

VM内のネットワーク設定を手動でおこなう

まずは、今回作成したVMにログインします。vagrant sshを実行します。

vagrant ssh
$ vagrant ssh
Last login: Fri Mar  1 15:21:31 2019

   __|  __|_  )
   _|  (     /   Amazon Linux 2 AMI
  ___|\___|___|

https://aws.amazon.com/amazon-linux-2/

ifconfigコマンドを実行し、現在のネットワーク設定を見てみます。IPv6の設定はあるものの、IPv4の設定がありません。

ifconfig
[vagrant@stdsv3 ~]$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
    inet6 fe80::215:5dff:fe14:2511  prefixlen 64  scopeid 0x20<link>
    ether 00:15:5d:14:25:11  txqueuelen 1000  (Ethernet)
    RX packets 5142  bytes 1149687 (1.0 MiB)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 474  bytes 92644 (90.4 KiB)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
    inet 127.0.0.1  netmask 255.0.0.0
    inet6 ::1  prefixlen 128  scopeid 0x10<host>
    loop  txqueuelen 1000  (Local Loopback)
    RX packets 0  bytes 0 (0.0 B)
    RX errors 0  dropped 0  overruns 0  frame 0
    TX packets 0  bytes 0 (0.0 B)
    TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

手動でIPアドレスを設定します。今回は「192.168.254.13/23」を設定します。設定後、ifconfigコマンドを実行し、意図したとおりにIPv4アドレスが付与されたことを確認します。

ip address add
[vagrant@stdsv3 ~]$ sudo ip address add 192.168.254.13/23 dev eth0

[vagrant@stdsv3 ~]$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.254.13  netmask 255.255.254.0  broadcast 0.0.0.0
        inet6 fe80::215:5dff:fe14:2511  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:14:25:11  txqueuelen 1000  (Ethernet)
        RX packets 5395  bytes 1185026 (1.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 617  bytes 110178 (107.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

「Hyper-V Internal with WinNAT」ネットワーク用のDefault Gatewayである「192.168.254.1」との通信ができるかどうか確認します。pingの応答はないものの、ARPの解決ができているので、L2レベルで通信できることは確認できました。
おそらく、当該ネットワークがPublic Networkになっているため、ホストOS側がICMPパケットを遮断しているものと推測できます。

ping
[vagrant@stdsv3 ~]$ ping 192.168.254.1
PING 192.168.254.1 (192.168.254.1) 56(84) bytes of data.
^C
--- 192.168.254.1 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1027ms

[vagrant@stdsv3 ~]$ arp -a
? (192.168.254.1) at 00:15:5d:14:25:10 [ether] on eth0

「192.168.254.1」をデフォルトゲートウェイとして追加します。

route add
[vagrant@stdsv3 ~]$ sudo route add default gw 192.168.254.1

[vagrant@stdsv3 ~]$ netstat -arn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.254.1   0.0.0.0         UG        0 0          0 eth0
192.168.254.0   0.0.0.0         255.255.254.0   U         0 0          0 eth0

Internet上の適当なIPアドレス(今回は8.8.8.8を利用)にpingを実行します。対象のIPアドレスからping応答が返ってくることが確認できました。

ping
[vagrant@stdsv3 ~]$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=14.8 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=11.8 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 11.853/13.340/14.827/1.487 ms

以上で、VM内からInternetに対して通信できることが確認できました。

VM内の設定を毎回手動でおこなうわけにもいきませんので、これを自動的に設定する方法を考えます。大きく以下の2通りの方法で実現することとなります。

  • VagrantによるVMインスタンス構築時、IPv4設定を併せて行う
  • ホストOS側でDHCPサーバを稼働させ、「Hyper-V Internal with WinNAT」ネットワークに所属するゲストOSがDHCPでIPv4設定できるようにする

Hyper-V通常VM用のネットワーク設定を自動化する

※続きます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitLabのコンテナをWindows上で構築したい

はじめに

  • Docker Desktop for Windowsをインストール
  • DockerのSettingsからボリューム設定に使用するドライブを共有化しておく。共有化する際にエラーが発生する場合は、Windows Firewallの設定か、3rd partyのセキュリティソフトでブロックされている可能性あり。それらの設定でDefaultSwichのネットワーク通信の許可を行うとうまくいくみたい。

GitLabのコンテナ作成&起動

  • GitLabのイメージがまだ無くても、docker runで自動的にイメージをダウンロードしてくれる。
  • 3つのボリュームを指定する(log, config and data)
    • この時configとlogにはWindowsのディレクトリを指定することが可能。(下記例ではE:/Docker/gitlab/config, E:/Docker/gitlab/logs)
    • 指定の仕方は以下のように<ドライブ名>:から始まるようにする(/E/Docker/gitlabではうまくいかなかった。)
    • dataにWindowsのディレクトリを指定するとうまくいかない模様…代わりにnamed volumeを指定する。 named volumeは”docker volume create gitlab”のようにコマンドを打っても作成できるが、無ければ自動で作成してくれるみたい。以下のコマンドをいきなり打つだけでOKだった。
docker run -i --hostname gitlab.sample.com --publish 80:80 --publish 22:22 --name gitlab --restart always --volume E:/Docker/gitlab/config:/etc/gitlab --volume E:/Docker/gitlab/logs:/var/log/gitlab --volume gitlab:/var/opt/gitlab gitlab/gitlab-ce:latest
  • 起動したらブラウザでlocalhostにアクセスすれば最初のページが見える。ちなみに上記コマンドを打っても処理がずっとループしているような現象がみられた。(ページにアクセスは可能だった。)その後、"docker stop gitlab"で停止した後、"docker start gitlab"で再開するとループは見られなかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS FargateでBlue/Greenデプロイを行う

概要

CodeDeployを利用したFargateでのBlue/Greenデプロイメントをコンソールから実装します。

基本的な内容は下記の記事を参考にしています。記事には書かれていないECRリポジトリのイメージの更新も含めて記載し、デプロイの流れを説明しています。
AWS CodeDeploy による AWS Fargate と Amazon ECS でのBlue/Greenデプロイメントの実装 | Amazon Web Services

全体の流れ

FargateでBlue/Greenデプロイメントを実装するための前提条件と、全体の流れを把握します。

前提条件

  • ECRにリポジトリが作成されていること
  • ECRにリポジトリにlatestタグがついたDockerメージがpushされていること
  • ECSクラスターが作成されていること

ECRにリポジトリを作成しDockerイメージをpushする方法については、下記の記事で手順を書いていますので、よろしければご覧ください。
LaravelアプリケーションをAWS上のDockerで動かす

Blue/Greenデプロイに必要なリソースを作成

  • IAMロールの作成
  • ALBの作成
  • タスクの定義の作成
  • ECSサービスの作成

詳細は後述しますが、ECSサービスの作成の中で、Deployment typeBlue/Green deploymentを選択することで、CodeDeployアプリケーションとデプロイメントグループが自動的に作成されます。CodeDeployを手動で作る必要がなく、とても便利です。

Blue/Greenデプロイをする

  • ECRのリポジトリに新しいDockerイメージをpushする
  • タスク定義のリビジョンを作成
  • 新しいリビジョンのタスクの定義を使用し、ECSサービスを更新する

ECSサービスを更新した時点で、CodeDeployによるBlue/Greenデプロイが実行されます。

これより先は、具体的な手順を解説していきます。

Blue/Greenデプロイに必要なリソースを作成

IAMロールの作成

ECSサービスが更新されると、CodeDeployによってECSへのデプロイが行われます。
そのため、CodeDeployがECSへのデプロイに関する操作を行えるようにIAMロールの作成を行います。

  • CodeDeploy用のIAMロールを作成し、AWSCodeDeployRoleForECSLimited管理ポリシーをアタッチ
  • タスク実行ロールまたはタスクロール上書きに対する iam:PassRole アクセス許可を、CodeDeploy用のIAMロールにインラインポリシーとして追加

Fargateの場合、タスク実行ロールが追加されていると思いますので、2つめの手順も忘れずに実行してください。

詳細な手順については、「Amazon ECS 開発者ガイド」のAmazon ECS CodeDeploy IAM Role を参照して下さい。

ALBの作成

443/tcp、8080/tcpを受け付けるALBを作成します。
この手順ではアプリケーションの都合上443/tcpとしていますが、80/tcpでも問題ありません。

ALBの作成の中で、セキュリティグループも新規作成します。
8080/tcpを任意の場所 (0.0.0.0/0) でも使用できるように、ルールを追加します。
スクリーンショット 2019-03-17 16.28.09.png

ルーティングの設定で、新しいターゲットグループを作成します。
名前は、Blue/Greenデプロイのためのターゲットグループであることがわかるように、stg-fargate-blueとしてます。Blue/Greenデプロイにおいて、ターゲットを切り替えるためのもう1つのターゲットグループは、ECSサービスの作成において作成するため、現時点ではターゲットグループは1つで大丈夫です。
スクリーンショット 2019-03-17 16.36.57.png

「ステップ 5: ターゲットの登録」は、特に設定は必要ありません。確認画面で設定を確認し、ALBを作成してください。
また、必要に応じてRoute53のレコードセットの追加を行います。

タスクの定義の作成

Fargateのタスクの定義の作成方法については省略させていただきます。

ECSサービスの作成

サービスの設定

項目
起動タイプ FARGATE
タスク定義 上記で作成したタスクの定義を選択
プラットフォームのバージョン LATEST
クラスタ 作成済みのクラスタを選択
サービス名 任意のサービス名
タスクの数 1

サービスの設定

項目
Deployment type Blue/green deployment (powered by AWS CodeDeploy)
Service role for CodeDeploy 上記で作成したCodeDeploy用のIAMロール選択

VPC とセキュリティグループ
VPCは、ALB作成で指定したVPC指定。
セキュリティグループは、作成済みのECSサービスのセキュリティグループを指定。

Elastic Load Balancing(オプション)

項目
ELB タイプ Application Load Balancer
ELB 名 上記で作成したALBを選択

「負荷分散用のコンテナ」をクリックして、負荷分散用のコンテナを登録します。

項目
リスナーポート 443:HTTPS をドロップダウンリストから選択
リスナープロトコル HTTPS
Test listener チェックあり
Test listener port 8080
Test listener protocol HTTP

スクリーンショット 2019-03-17 17.04.06.png

Additional configuration
ここでは、Blue/Greenデプロイで使用する2つのターゲットグループに関する設定を行います。
このターゲットグループをCodeDeployが切り替えることによって、Blue/Greenデプロイが可能になります。

ターゲットグループ1は、ALB作成時に作成したstg-fargate-blueを選択します。
スクリーンショット 2019-03-17 17.06.54.png

ターゲットグループ2は、新規で作成します。名前は、stg-fargate-greenとしておきます。なお、ターゲットグループ名、ヘルスチェックパスについては環境に合わせて変更してください。
スクリーンショット 2019-03-17 17.08.37.png

上記の設定が完了したら、ECSサービスを作成します。
この時点で、ALBを確認するとターゲットグループが作成されていることが確認できます。
スクリーンショット 2019-03-17 17.12.13.png

CodeDeployについても確認してみると、アプリケーションとデプロイグループが作成されています。(この例だと名前が微妙ですね。)
スクリーンショット 2019-03-17 17.19.09.png

デプロイグループのデプロイ設定を変更

デプロイグループのデフォルトの設定では、デプロイされた際に、新しくデプロイが行われた側のターゲットグループに、トラフィックが自動で流れ始めてしまいます。
今回は、動作確認をしてからターゲットグループの変更を行いたいため、トラフィックの再ルーティングするタイミングを指定します。今回は15分後に、再ルーティングされるように変更します。この設定をすることで、15分間待たなくても手動で再ルーティングすることも可能です。詳細は後述。

また、デフォルトの設定では、タスクの正常な展開後(再ルーティング後)1時間待ってから元のタスクが終了します。今回は30分後にタスクが終了するように設定を変更します。

デプロイグループを選択し、「編集」ボタンを押下することで、デプロイ設定画面が表示されます。

スクリーンショット 2019-03-17 18.03.57.png
スクリーンショット 2019-03-17 17.59.28.png

Blue/Greenデプロイをする

ECRのリポジトリに新しいDockerイメージをPushする

ECRのリポジトリは下記の通りとなっています。
スクリーンショット 2019-03-17 17.36.13.png

タグ1.0.2,latestをつけたDockerイメージをECRのリポジトリにpushします。
latestが最新のイメージのみについています。古いイメージについていたlatestは自動的に剥がされています。
スクリーンショット 2019-03-17 17.53.55.png

タスク定義のリビジョンを作成し、ECSサービスを更新する

タスクの定義の新しいリビジョンを作成し、ECSサービスを更新します。
このサービスを更新した時点で、CodeDeployによるデプロイが開始されます。

CodeDeployからデプロイのステータス、トラフィック移行の進行状況を確認できます。
スクリーンショット 2019-03-17 18.10.32.png

タスクを確認すると、新旧のタスクのリビジョンが起動していることを確認できます。
スクリーンショット 2019-03-17 18.12.05.png

ALBのエンドポイントの8080番ポートにアクセスし、正常に動作していることを確認します。
問題がなければ、デプロイしたターゲットグループにトラフィックを向け、リリースを実施します。

CodeDeployの「トラフィックの再ルーティング」ボタンを押下することで、デプロイしたターゲットグループにトラフィックが流れ始めます。
スクリーンショット 2019-03-17 18.19.09.png

スクリーンショット 2019-03-17 18.21.35.png

ALBのリスナーを確認すると、ターゲットグループが変更されていることを確認できます。
HTTPSの転送先のターゲットグループがstg-fargate-blueからstg-fargate-greenに変更されています。
スクリーンショット 2019-03-17 18.24.30.png

旧タスクの待機時間が終了すると、旧タスクが停止されデプロイが完了します。
スクリーンショット 2019-03-17 18.50.10.png

ロールバック機能

デプロイに問題が発生した場合は、展開を停止してロールバックすることができます。
サービスのデプロイタブに表示されている「Stop and rollback deployment」ボタンを押下することで、展開が停止されます。

スクリーンショット 2019-03-17 18.17.29.png

まとめ

ECSでCodeDeployを使用したBlue/Greenデプロイメントを利用することで、簡単にBlue/Greenデプロイができました。CodePipelineを利用したデプロイもサポートされているので、さらにデプロイが簡単になりますね!

参考記事

参考にさせていただきました。ありがとうございます。
AWS CodeDeploy による AWS Fargate と Amazon ECS でのBlue/Greenデプロイメントの実装
ECSでCodeDeployを使用したBlue/Green Deploymentがサポートされたので早速試してみた #reinvent | DevelopersIO

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む