- 投稿日:2019-03-18T23:01:11+09:00
[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/
ポチッとするとSign Inせよと言われるので、アカウントがない場合はCreate Accountのリンクからアカウントを作成。私はアカウントなかったので作りました。
上のメニューのとこからSign Inすると、Get Dockerボタン押下でdmgファイルダウンロード出来ます。やったね。
これでインストール出来たのか不安になったので、
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.10Python3.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時間もかからないと思います。ここから更にいじって遊んでみたいので、後日色々試していきます。
- 投稿日:2019-03-18T20:30:01+09:00
【Vagrant/Docker】Windowsでも開発できるようにVagrant + Docker の環境を作る
Docker for Windows
とVagrant/VM VirtualBox
が相容れない問題VirtualBox 6.0 から Hyper-V と共存できるはず - Qiita
を見てやっと共存できるようになったんか~~と思って試してみたんですが、
やっぱダメだったんであきらめてVagrant
上でDocker
動かすことにしました。Vagrant + Docker の環境を作っていく
前回は
Docker for Windows
で環境構築しましたが、今回はVagrant + Docker
で環境構築していきます。実行環境
OS:Windows 10 Pro 64bit
RAM:8GBVagrantfileを用意する
以下の記事を参考にさせていただきました
参考:centos7を起動してすぐDockerできるVagrantfile - Qiitaただ、記事に書いてある文のままだとDocker Composeのバージョンが古いので、
GitHubのReleases · docker/composeを 開き、最新のバージョンに書き換えました。VagrantfileVagrant.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 エラー(Guest Additions - vboxsf) - Qiita
- vagrant up時に「mount: unknown filesystem type '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おわり
- centos7を起動してすぐDockerできるVagrantfile - Qiita の記事がとても参考になりました。ありがとうございました。
![]()
参考URL
- 投稿日:2019-03-18T18:04:03+09:00
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が所属するグループに紐づけましょう.nvidia-dockerインストール
公式GitHubのQuickstartの所を実行してください.
なお,上記の方法でdockerをインストールした場合は最初から最新のdockerが入ってるはずなので,二番目のリポジトリを追加するところから始めて大丈夫です.Docker Imageの用意
公式が配布するDocker Imageを入手する
メジャーなフレームワークだったら,Docker Imageが配布されている場合があるので,DockerfileをBuildする手間が省けます.
ただし環境をカスタマイズしたいなら,後述のようにDockerfileからビルドした方が良いです.Tensorflowの例
- TensorflowのDockerHub にアクセスして,Tagsから欲しいバージョンのimageを見つけます.
- 次のコマンドを入力して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.ymlversion: '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はもはや機械学習関係のことを行うのに必須な環境と言ってよいでしょう.みなさんの健闘をお祈りします.
- 投稿日:2019-03-18T17:20:27+09:00
Dockerコンテナ上でJavaプログラムを動かすときにLANG環境変数を設定すると日本語のファイル名が文字化けする問題
概要
CentOS の Docker コンテナ上で Java プログラムを動かしていたところ、日本語のファイル名を含むファイル一覧の取得で謎の文字化けが発生しました。
Sample.javaimport 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プログラムで日本語ファイル名が文字化けする
- 投稿日:2019-03-18T15:09:19+09:00
オレオレめもDockerでよく使うコマンド
- 投稿日:2019-03-18T13:23:07+09:00
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.30bootloaderの書き込み
ビルド環境は、Docker上で構築できたのですがDocker for windowsではシリアルポートの認識がDocker上でできないようなのでビルドしたbootloaderの書き込みはWindowsかWSL上のLinuxから行います。
そのため書籍には書き込み時に行っているobjcopyをビルド時に行うように修正する必要があります。それ以外は書籍に指示された方法でビルドしたオブジェクトを書き込みます。
結局は、ビルドをDocker上のUbuntuで行い、Bootの書き込みをWindowsかWSLで行うという統一性のない手続きになってしまっています。どなたか、Docker for windowsでデバイスを認識する方法をご存知ありませんか?最後に
注意として、この環境はあくまでgcc7系でH8300のクロスコンパイラが動き、kozosの動作が確認できたという実験的な環境です。書籍として学習する際に余計なエラーが出る可能性がありますのでまずは書籍に指示されている環境での学習をおすすめします。
参考
- 投稿日:2019-03-18T09:33:34+09:00
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 Isolation
とHyper-V Isolation
という 2 つの分離レベルがある。分離レベル : Process Isolation
Kernel を Host と共有し、Container は 1 つの Process として動作する方式。
Windows Server Container, Windows Process Container とも呼ばれる。同じ方式を取っている Container Runtime としては runC や rkt 等がある。
- 利点
- 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 である hcsshim や C# 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/hcnOCI, 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
ContainerWindows
Process
ContainerResource 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その他、以下動画や資料などで情報を補完した。
- Windows Server & Docker - The Internals Behind Bringing Docker & Containers to Windows - Black Belt - Youtube
- 同動画スライド - slideshare
- Deep dive into Windows Server Containers and Docker – Part 2 – Underlying implementation of Windows Server Containers
- コンテナー - Windows Server コンテナーで Windows 開発者が Docker を使えるように - MSDN Magazine
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 の細やかな管理を可能としている。
与えられた Capability は、専用の特殊な Group ( Flags:
Capability
, Name:APPLICATION_PACKAGE_AUTHORITY\XXXXXXX
( SID: S-1-15-3-XXXXX ) ) への所属をもって管理される。
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-conceptsWindows へ向けた多くのアプリケーションは 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.exeServices
Windows は Linux とは違い、直接 Syscall せずに DLL を介して kernel Mode にアクセスする。
しかし、これら DLL が依存する System Service が User Mode に存在する為、Container はこれら System Service を丸ごと含む必要がある。
docker run ~
すると、smss.exe
の下に新たにsmss.exe
ができ、またcsrss.exe
とwininit.exe
Tree が新たに起動されるのが確認できる。
Windows Internals によると、Silo Namespace 作成後、
- Job Object に関連付けられた Smss を作成
- Smss が Session 0 の初期化処理として、
Wininit.exe
,Csrss.exe
を起動Wininit.exe
がservices.exe
,Lsass.exe
を起動し、自動起動サービスが立ち上がっていく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"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
で見られる。
Silo の情報はこんな感じ。
ちなみに、Host のは!Silo -g Host
で見られる。
Globals はこんな感じ。
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-notificationsPS> docker run -it --isolation process -m "100m" mcr.microsoft.com/windows/nanoserver:1809 cmd.exePS> docker inspect dd | wsl jq '.[0].HostConfig.Memory' # 104857600この Container の Job Object を ProcessExplorer で見てみると、Memory Limit がかかっているのが確認できる。
Process
● Container 内
Container 内から見えるのは、Job Object に関連付けられた
smss
が立ち上げた子プロセスに限られる。
(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 を共有しているので当然か。
不思議なのが、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 が関連付けられているのが分かる。
Job を調べると、Silo
Flag が付けられている。
その Silo は Server Silo Type で、RootDirectry が\Silos\1192
と分かる。
◆ 結論
答えはわからないが、 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 を持っていなかった。
W32pServiceTableFilter
やW32pArgumentTableFilter
辺りなのかなぁとは思いつつ、これ以上 Deep な世界に行く能力もなく、ここらへんで断念。◆ 結論
詳細な動作原理が分からなかった。
そもそも、Docker の
--security-opt
に Windows で使えそうなセキュリティオプションがなく、カスタマイズのしようが無い。
https://docs.docker.com/engine/reference/run/#security-configurationSandboxing for capability
● AppContainer
まずは、AppContainer についてざっと見てみる。
Store App である Calculator の場合。
インターネット接続 ( S-1-15-3-1 )
Capability が付与されていると分かる。
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 昇格後と同等の相当高いレベル。
Group にも、Capability に関するものが何もない。
もしかして、全く違う機構で動いているんだろうか。● 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 の設定から調べた。
新たに JID2136
の Container を立ち上げたとする。
\Silos\2136\GLOBAL??\C:
と\Silos\2136\GLOBAL??\Volume{0b4ac2ae-ab3f-4861-bc1d-1504bf438d6b}
の Link はHarddiskVolume68
を指している。
HarddiskVolume68
は、Disk 2 の Partition 2 上に mount されている。( Volume と Disk の関連を確実に見つけるなら、この方法で で )
これを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.vhdxDisk 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 機能を実現していると考えられる。
◆ 結論
これら情報を照らし合わせると、
- Container 起動
C:\ProgramData\Docker\windowsfilter
以下に Container ID と同名のフォルダ作成- その中に Virtual Hard Disk ( 差分 Disk ( 詳細後述 ) ) を作成、Volume として利用可能な状態とする
- Silo Namespace 内の
\Global??\C:
や\Global??\Volume{...}
の参照先を 3 の Volume に- Container の Boot 時には、
\SystemRoot
の参照先である\Global??\C:\windows
から Windows を立ち上げると予測できる。
実験
![]()
試しに、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 はドライブ文字またはマウント ポイントを正常に割り当てました。UnionFS
Virtual Hard Disk レベルで切り替えがされていることは分かった。
しかし、Docker の特徴でもある、差分毎の Image Layer や Copy on Write 等については、どの様に実現するのだろうか。
通常、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
実験
![]()
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 を格納するフォルダが追加されている。OtherTerminalPS> 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 ArchiveContainer の中で、ファイルを編集してみる。
編集された影響は、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
実験
![]()
まずは、この
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
に書き込まれているのが確認できた。
どうやら 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> WindowsPS> 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> WindowsPS> docker inspect be13f4b02824 | wsl jq '.[0].Mounts' # [ # { # "Type": "bind", # "Source": "c:\\src", # "Destination": "c:\\src", # "Mode": "", # "RW": true, # "Propagation": "" # } # ]● 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 であることが分かる。
また、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 でもしないと分からなそう。
◆ 結論
という事で、これら情報を照らし合わせると、
- Docker Image を Pull
C:\ProgramData\Docker\windowsfilter
以下に Layer 毎にフォルダを作成C:\ProgramData\Docker\windowsfilter\\<<>GUID>\\Files
に Layer のファイル展開docker volume create <<dir_name>>
した場合は、C:\ProgramData\Docker\volumes
以下に<dir_name>
フォルダを作成- Container 起動
- Change Root の手順
- 何らかの方法 で、Layer 情報を wcifs に伝える or wcifs が参照するデータ ? ファイル ? Object ? に設定する
- Container 内で File Write が発生すると、sandbox.vhdk 上に変更が書き込まれ reparse points 先を変更
- Container 内で File Read が発生すると、reparse points 先を読み込む
と予測できる。
Registry
Registry も Windows においては重要なデータである。
App-V 1703 Virtual Registry and Containers?● Registry とは
そもそも Registry は、Hive というファイルを
HKLM
,HKCU
やHKLM\SECURITY
等に mount して 1 つの大きな Tree Structure を構築したもの。
以下表を見ると分かる通り、mount, SymLink, 疑似 FS と NTFS よりも Linux のそれに近い。
実体 参照 HKEY_CLASSES_ROOT\ HKLM\SOFTWARE\Classes
とHKCU\Software\Classes
を merge した仮想 KeyHKEY_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_MACHINE と HKEY_USERS を管理すれば良いという事になる。
● 実践
という事で、まずは Process Explorer で確認してみたが、普通にアクセスしているようにしか見えない。
どういう事 ?● Hive ファイル
よく分からないので、新たに Container を立ち上げてその過程を Process Monitor で確認すると、
\Registry\WC\Silo ~
というレコードが沢山出てきた ( 通常は、HKCU\…
やHKLM\…
になる )。
このレコードの最初の出処まで遡って見てみると、\Registry\WC\Silo9dd9eeab-...-b0acd579bc17system
等が RegLoadKey されていた。
その備考欄には
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 をいくつも見つけた。
・\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 ] + _DeltaContainer の 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
も存在している。
実験
![]()
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
と分かった。
+ = ・System Hive in Container
場所 : Q:\Windows\System32\config\SOFTWARE
参照元 : C:\ProgramData\Docker\windowsfilter\0b497555b76d5...\Files\Windows\System32\config\SOFTWAREWindows は起動時に
smss
がC:\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 以降、以下機能が追加されている。
- UAC 昇格前のアクセスを deny するのではなく、仮想の Registry Tree に Copy on Write する機能
- ただし、仮想化が許されるのは
HKEY_LOCAL_MACHINE\Software
以下のみ ?完全に同じものではないようだが、似た原理は利用しているのかもしれない。
◆ 結論
これら情報を照らし合わせると、
- Container 起動
- Container 内にある
C:\WcSandboxState\Hives\*
を\REGISTRY\WC\Silo + <Random GUID>
に mount- Container 内にある
C:\Windows\System32\config\*
を\REGISTRY\WC\Silo + <Other Random GUID>
に mount- Container 内で Registry Write が発生すると、
\REGISTRY\WC\Silo + <Random GUID>
以下に書き込まれる- Container 内で Registry Read が発生すると、
\REGISTRY\WC\Silo + <Random GUID>
と\REGISTRY\WC\Silo + <Other Random GUID>
を merge した Virtual Registry から読み込むと予測できる。
Network
次は、Network がどうなるか。
Network については、資料がとても充実している。
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/architecture
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-drivers-topologies
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-isolation-security
- https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/advanced#dhcp-ip-assignment-not-supported-with-l2bridge-networks
- https://blogs.technet.microsoft.com/virtualization/2016/05/05/windows-container-networking/
- https://docs.microsoft.com/en-us/windows-server/networking/sdn/manage/connect-container-endpoints-to-a-tenant-virtual-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 : 52542NAT 名が 『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 ActiveStoreBind 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 ActiveStoreDHCP から 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-securitySDN 環境を用意するのは辛いので、取り敢えず 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』が有効になっているのが分かる。
MAC Address Rewrite
どのように MAC Address が書き換わるのかを確認する。
Container を参加させた Subnet 内の適当な PC へ Packet を送信し、その時の Arp Table の状態を確認してみる。HostPS> 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 ActiveStoreContainerPS> 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 ActiveStore192.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-modePS> 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]調べてみると、同様の現象が報告されている。未解決。
- Fail to start tasks/services in Docker Swarm: hnsCall failed in Win32: The parameter is incorrect - stackoverflow
- How to troubleshoot docker on windows : hnsCall failed in Win32: The parameter is incorrect. (0x57) - DOCKER COMMUNITY FORUMS
エラーの内容的に、
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.8PS> 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/
- 投稿日:2019-03-18T09:33:22+09:00
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/103000Container と Windows
1. 背景
上記紹介記事にもあるが、元々 Docker は Linux の持つ cgroup, Namespace, chroot 等の機能を利用して構築 されており、他の Platform へ簡単に移植することはできなかった。
その為、Windows や Mac OS では、VirtualBox や xhyve, Hyper-V 上に Linux VM を構築し、それを Host Machine からできるだけ透過的に操作できるように工夫していた。
しかし、Microsoft は早い段階から Windows Native な Container の実現に前向だった。
2. 沿革
● 2013/3 - Docker を OSS 化
この頃 Windows ユーザは、VirtualBox 等に VM を立てて、その中で Docker を利用していた。● 2014/4 - Boot2Docker v0.2 がリリース
これにより、VM, Guest OS, Docker, MSYS base Terminal がワンパッケージで導入され、アイコンワンクリックで Docker が使えている 風 に見えるようになった。
とはいえ、 Volume や Network の統合は無く、結局現実に呼び戻される。● 2014/10 - Microsoft と Docker が協業を発表
Windows Server への Docker Engine 統合、Windows Native Client 開発、Dockerhub による Windows Container Image 管理の実現を発表した。● 2014/11 - Docker CLI for Windows がリリース
ここで初めて Windows Native で動く Docker Client が生まれた。
しかし、相変わらず Docker が動いているのは VM 上の Linux だ。● 2015/5 - Windows Server 2016 Technical Preview 2 リリース
Windows Nano Server が提供される。● 2015/8 - Windows Server 2016 Technical Preview 3 リリース
念願の Windows Server Container が提供される。● 2015/11 - Windows Server 2016 Technical Preview 4 リリース
少し遅れて Hyper-V Container が提供される。● 2016/4 - Windows Server 2016 Technical Preview 5 リリース
Windows Container Image の DockerHub での利用が可能に。
ただし、この時点での WindowsServerCore Image はディスク上で 約 9 GB, WindowsNanoServer Image でも 約 600 MB と、Linux Container 並の Portability を実現するには少し辛いサイズであった。● 2016/7 - Docker for Mac/Windows が正式リリース
OS Native Hypervisor ( Win: Hyper-V, Mac: xhyve ) を利用した Docker アプリケーション。
Docker が動くのが VM 上の Linux であることに変わりは無いが、Volume や Network 周りが見事に統合されていて、ホストマシン上で直接操作しているかのような使用感が得られる。● 2016/8 - Windows 10 Pro が Hyper-V Container に対応
Desktop OS でも Windows Container が利用できるようになった。● 2017/9~10 - Windows 10 Fall Creators Update と Windows Server 1709 で LCOW ( Linux Containers on Windows ) に対応
Windows 版 Docker Engine での Linux Container 立ち上げが可能に。● 2018/8 - Windows Container Image のサイズがどんどん小さくなっていく
この時点での WindowsServerCore Image はディスク上で 約 3.6 GB, WindowsNanoServer Image でも 約 100 MB 未満● 2018/8 - Docker for Windows/Mac の 2.0.0.0 がリリース。同時に名称を Docker Desktop for Windows/Mac に変更
● 2019/2 - Docker Desktop 2.0.0.2 で Windows 10 Pro が Windows Server Container に対応
いよいよ環境が全て整った。
3. 用語の整理
Linux Container
Linux Kernel で動作する Container のこと。
Windows Container
Windows の NT Kernel で動作する Container のこと。
場合によって呼び方は異なるが、多分公式にもこう呼ばれているはず。
Windows Server Container
Windows Container の実現方法の 1 つ。
Process レベルで分離される。Windows process container とも呼ばれる。
Hyper-V Container
Windows Container の実現方法の 1 つ。
kernel レベルで分離される。Windows Hyper-V container とも呼ばれる。
LCOW ( Linux Containers on Windows )
Windows Native Docker Engine によって Linux Container が動かせる機能。
技術的には Hyper-V Container とほぼ同じで、Hyper-V 上で小さな Linux VM を立ち上げて、そこで実行される。Docker Desktop エコシステム復習
以降では、Docker Desktop + Linux Container エコシステムについて復習していく。
従来の構成は、Hyper-V 上に設けられた完全な VM 上にある Docker Daemon に、Windows 上の Docker Client で接続して操作する。
↓ ざっくりとしたイメージ図
以下、重要な部分だけ確認していく。
Windows
まずは、Windows 側がどうなっているかを見ていく。
起動している関連サービスは、以下。PS> ps | wsl grep -i -e ProcessName -e '---' -e docker -e vpnkit # Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName # ------- ------ ----- ----- ------ -- -- ----------- # 577 11 10408 18672 11.89 27256 13 com.docker.proxy # 4385 111 184944 50004 20764 0 com.docker.service # 1042 64 119364 91780 17.83 23480 13 Docker Desktop # 35 3 484 2052 8376 0 Docker.Watchguard # 35 4 512 2068 8992 0 Docker.Watchguard # 255 16 20516 29680 7912 0 dockerd # 641 66 20412 12184 15.80 28084 13 vpnkitdockerd, 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/linuxkitYAML 定義を元に Image がビルドされる。
Desktop Docker の場合は、インストール時 ( アップグレード時も? ) に最新 Image を取得して、Hyper-V 上に展開してくれる。接続
どんな Image なのか調査する為、Linuxkit に繋ぎたかったのだが、sshd が見つからなくて、Hyper-V Manager からの接続もできないので、裏技 を使って中に入る。
$ uname -a # Linux docker-desktop 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 Linux ### ビルド時に利用された定義は、以下にコピーされている $ cat /etc/linuxkit.yml # kernel: # image: linuxkit/kernel:4.9.125-4ffac525e6a57ccc3f2a8ae0fb96f12169027759-amd64 # cmdline: console=ttyS0 page_poison=1 vsyscall=emulate panic=1 # ...Build
LinuxKit のビルドは、
linuxkit.yml
のkernel
→init
→onboot
→onshutdown
→services
→files
セクションの順に実行される。全ての処理が、Container Image の展開か Container の実行で行われる。
kernel
セクションで kernel を/boot
フォルダに展開し、init
セクションで、Containerd, RunC, getty 等が導入されている。$ ctr version # Client: # Version: v1.1.2 # Revision: 468a545b9edcd5932818eb9de8e72413e616e86e # # Server: # Version: v1.1.2 # Revision: 468a545b9edcd5932818eb9de8e72413e616e86e $ runc -v # runc version 1.0.0-rc5+dev # commit: 69663f0bd4b60df09991c08812a60108003fa340 # spec: 1.0.0
onboot
セクションにある定義は、直接 runc を呼び出して実行される。
各種初期設定を行った残骸が残っている。$ runc list # ID PID STATUS BUNDLE CREATED OWNER # 000-metadata 0 stopped /containers/onboot/000-metadata 2019-02-20T21:17:29.2710123Z root # 001-sysfs 0 stopped /containers/onboot/001-sysfs 2019-02-20T21:17:30.6890069Z root # 002-binfmt 0 stopped /containers/onboot/002-binfmt 2019-02-20T21:17:31.6772885Z root # 003-sysctl 0 stopped /containers/onboot/003-sysctl 2019-02-20T21:17:32.0187455Z root # 004-format 0 stopped /containers/onboot/004-format 2019-02-20T21:17:32.5950764Z root # 005-extend 0 stopped /containers/onboot/005-extend 2019-02-20T21:17:33.834064Z root # 006-mount 0 stopped /containers/onboot/006-mount 2019-02-20T21:17:41.5659989Z root # 007-swap 0 stopped /containers/onboot/007-swap 2019-02-20T21:17:43.3930679Z root # 008-move-logs 0 stopped /containers/onboot/008-move-logs 2019-02-20T21:17:50.9157579Z root # 009-mount-docker 0 stopped /containers/onboot/009-mount-docker 2019-02-20T21:17:51.5884119Z root # 010-mount-kube-images 0 stopped /containers/onboot/010-mount-kube-images 2019-02-20T21:17:52.2584598Z root # 011-bridge 0 stopped /containers/onboot/011-bridge 2019-02-20T21:17:52.5884599Z root # 012-vpnkit-9pmount-vsock 0 stopped /containers/onboot/012-vpnkit-9pmount-vsock 2019-02-20T21:17:52.9334929Z root # 013-rngd1 0 stopped /containers/onboot/013-rngd1 2019-02-20T21:17:53.5926046Z root # 014-windowsnet 0 stopped /containers/onboot/014-windowsnet 2019-02-20T21:17:53.963468Z rootLinuxkit は、基本的に読み込み専用なので、全てのサービスを 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---shdockerd
肝心の
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: truePersistence 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 swapVolume Sharing
Docker for Windows で Shared Driver に設定されたドライブは自動で共有フォルダとなる。
↓ File 共有に出された Drive は、Linux 側で
/host_mnt/*
というパスに変換されて mount される。
( 多分 Docker Client が勝手に Path 変換をしているんだろうと予想 )
その実態は、
services.linuxkit/docker
コンテナ内の/host_mnt/*
に CIFS で mount される。$ ctr --namespace services.linuxkit tasks exec --exec-id 1000 docker mount -l | grep host_mnt # //10.0.75.1/C on /host_mnt/c type cifs (rw,relatime,vers=3.02,sec=ntlmsspi,cache=strict,username=<<Windows User>>,domain=<<Windows PC Name>>,uid=0,noforceuid,gid=0,noforcegid,addr=10.0.75.1,file_mode=0755,dir_mode=0777,iocharset=utf8,nounix,serverino,mapposix,nobrl,mfsymlinks,noperm,rsize=1048576,wsize=1048576,echo_interval=60,actimeo=1)Network
dockerd Container は LinuxKit Host の Default Network Namespace と同じ Namespace が割り当てられているので、以降は LinuxKit Host のネットワーク環境として見ていく。
現在、おおよそ以下の NIC が存在している。
◆ Host Namespace
NIC Name IP master Default
Routelo 127.0.0.1/8 eth0 192.168.65.3/28 ○ hvint0 10.0.75.2/24 docker0 172.17.0.1/16 vethXXXXXXXXX@ifXXX docker0 cni0 10.1.0.1/16 vethXXXXXXXXX@eth0 cni0 tunl0@NONE ip6tnl0@NONE ◆ Container Namespace
NIC Name IP master lo 127.0.0.1/8 eth0@ifXXX 172.17.X.X/16 tunl0@NONE ip6tnl0@NONE Interface: eth0
一見、一番簡単そうに見えて一番難しい NIC。
Linuxkit の Default Network Namespace の Default Route デバイス。
192.168.65.0/28
には、Default Gateway である192.168.65.1
と、Windows Host を示す192.168.65.2
がある。$ ip n show dev eth0 # 192.168.65.1 lladdr f6:16:36:bc:f9:c6 ref 1 used 0/0/0 probes 1 REACHABLE # 192.168.65.2 lladdr f6:16:36:bc:f9:c6 used 0/0/0 probes 4 STALE一見すると物理 NIC にも見えるが、実は TAP 仮想デバイスであり、その裏では vpnkit というツールが Hyper-V Socket, vsock を利用して通信のトンネリング・仲介をしている。詳細な原理は後述。
Interface: hvint0
Docker Desktop は導入時、
DockerNAT
という仮想 Switch を作る。
LinuxKit はそのDockerNAT
に接続された状態で起動される。Windows 側には
イーサネット アダプター vEthernet (DockerNAT)
という仮想 NIC が作成され、LinuxKit 側にはhvint0
という NIC が作られ ( 正確には、起動時にeth0
だった物理 NIC をリネームしている )、どちらもDockerNAT
に接続される。
この経路は主にドライブの mount 用に利用されるようだ。Network: docker0, vethXXXX@ifXXX
Docker が構築するいつものネットワーク。
各 Container は、Host とは違う Network Namespace をそれぞれ持つ。
veth のペアは、一つは Host Namespace に、もう一つは各 Container Namespace に配置される。
docker0
は bridge であり、veth に master としてリンクされている。
また、docker0
は IP Address も持っており、各 Container Namespace の Default Gateway となっている。また iptables の IPマスカレード機能により、
docker0
を通る Container の Outbound Packet 全て送信元 IP 変換がなされる。Network: cni0, vethXXXX@eth0
CNI プラグインで利用されるネットワーク。Kubernetes が有効になっていると作成される。
CNI ( Container Network Interface ) とは、Container の Networking を担当するプラグインの I/F 仕様。
多くの Container Runtime や Orchestrator が登場する中、各社独自の Networking 実装による重複を避ける目的がある。各 Pod は、Host とは違う Network Namespace をそれぞれ持つ ( Pod 内の Container は同じ Network Namespace )。
今回は具体的な CNI プラグイン実装が入っていないが、例えば Flannel 等でクラスタが構築されれば多分以下のようになるはず。今回は Kubernetes は射程外なので ( というか、自分自身が詳しくもないので ) あまり踏み込まない。
Tunnel: tunl0@NONE, ip6tnl0@NONE
稀に遭遇する謎のデバイス。一体何のためにあるのか分からなかった。
ちなみに、Container の中にもいる。$ ip tunnel show # tunl0: unknown/ip remote any local any ttl inherit nopmtudisc $ ip addr show tunl0 # 3: tunl0@NONE: <NOARP> mtu 1480 qdisc noqueue state DOWN qlen 1 # link/ipip 0.0.0.0 brd 0.0.0.0 $ ip link set dev tunl0 up $ ip addr show tunl0 # 3: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN qlen 1 # link/ipip 0.0.0.0 brd 0.0.0.0 $ ping -I tunl0 172.17.0.2 # PING 172.17.0.2 (172.17.0.2): 56 data bytes # . # . # . # ( 沈黙 )calico の Github Issues にある情報だが、IPIP カーネルモジュールが読み込まれたときの副作用で作られるとの情報あり。
- tunl0 device present inside containers - github.com/projectcalico/calicoctl
- tun10@NONE interface inside the pods is of no use - github.com/projectcalico/calico
- what does none in “tun0@none” stand for? - Unix & Linux Stack Exchange
Docker 公式にもしれっと居たりする。そして触れられないという。
https://docs.docker.com/network/none/ Host-Guest 間 socket 通信
古くは VMWare の VMCI Socket、最近では Qemu で使われる virtio-vsock ( Address-Fammily = AF_VAOCK ) という技術を使うことで、Network を一切介さずに VM Guest と Host の間で通常の BSD socker API を使った通信が可能となる。
メモリを共有し、その上でデータ交換するので高速な通信が可能となる。https://medium.com/@mdlayher/linux-vm-sockets-in-go-ea11768e9e67
https://pubs.vmware.com/vsphere-51/index.jsp?topic=%2Fcom.vmware.vmci.pg.doc%2FvsockAbout.3.2.html
https://wiki.qemu.org/Features/VirtioVsockそして 2017 年、ついに Hyper-V にもこの Host-Guest 間 socket 通信ができる機能が追加された。
Docker Desktop では至る所でこの Hyer-V Socket が利用されている。Hyper-V Socket
Hyper-V Host と Guest との間で通信を行う Socket。2017 年頃に Windows 10, Windows Server 2016 に導入された。
Network を介さず、VMBus 経由でやり取りするのでハイパフォーマンス。
https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service● Socket Address Family
この Socket を実現するため、socket のアドレスファミリに
AF_HYPERV
が追加された。
Linux Guest 側は vsock を利用する。● Guest Communication Service
Hyper-V Socket を利用するには、まずは Windows に Guest Communication Service というものを登録する必要がある。
これは、Unix Domain Socket で言うところの File Path のような、通信チャンネルの識別子的なもので、Windows Host の Registry に登録される。
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices
にある。見てみると、既に Docker や Kubernetes 関連のサービスがいくつか登録されているのが分かる。
● Service GUID 命名規則
Hyper-V Socket と vsock では通信先アドレスの指定方法が違っていて、Hyper-V Socket の場合は
VM GUID
とService GUID
を指定するが、vsock の場合はcid
とport
( 0 ~ 0x7FFFFFFF の数値 ) を指定する。これらを両立させる為に、Service ID としての GUID を決める際には以下のルールに則る。
[[ Port Number ]]-FACB-11E6-BD58-64006A7986D3例えば、Service ID
00000948-FACB-11E6-BD58-64006A7986D3 ( ElementName : Docker API )
について通信したい場合、以下の様な設定になる。
- Hyper-V Socket
- VM GUID
(Get-VM -Name 'DockerDesktopVM').Id
- Service GUID
00000948-FACB-11E6-BD58-64006A7986D3
- vsock
- cid
VMADDR_CID_HOST
( これ一択 ? )- port
- 0x00000948 →
2376
また、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-64006A7986D3vsudd & com.docker.proxy.exe
Docker API 通信を Hyper-V socket で Tunneling して
docker.sock
へと Proxy するサービス。
これにより、Windows Host からdockerd
が操作できる。
https://github.com/linuxkit/virtsock/tree/master/cmd/vsudd
LinuxKit 上で起動した
vsudd
は、vsock(cid=VMADDR_CID_ANY, Port=00000948)
で待ち受けて、受け取ったデータをdocker.sock
Unix Domain Socket へと Proxy する。Windows Host 側では、サービスにより起動された
com.docker.proxy.exe
が Named Pipe//./pipe/docker_engine
で待ち受けて、受け取ったリクエストをhyperv-listen://XXXXXXXX-XXXX-.../00000948-FACB-...
宛に転送する。Docker Client から dockerd 宛に指示を出す時は、
docker -H npipe://./pipe/docker_engine ~
となる。VPNKit
Hyper-V socket/vsock を利用して様々な通信の仲介・Tunneling をするための Toolkit。 OCaml, Go, C で実装されている。
https://github.com/moby/vpnkit/以下、主要なサービス。
- On Linux Guest
- vpnkit-tap-vsockd
- Guest Communication Service : Docker VPN proxy ( vsock port : 0x30D48B34 )
- Container, LinuxKit Host から外部ネットワークへの通信経路を提供
- TAP デバイス
eth0
を設置eth0
( vpnkit-tap-vsockd ) ⇔vpnkit.exe
を Hyper-V socket で Tunneling- vpnkit-forwarder
- Guest Communication Service : Docker port forwarding ( vsock port : 0x0000F3A5 )
- Windows Host から Linxkit Host への Port Forwarding 機能を提供
vpnkit.exe
⇔vpnkit-forwarder
を Hyper-V socket で Tunnelingvpnkit-forwarder
⇔Container
間の 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
LinuxKit 内で外向け Packet が Default Route のeth0
に到着すると、vpnkit-tap-vsockd
はそれを読み取り、Encapsulation してvsock(cid=VMADDR_CID_HOST, Port=30D48B34)
へ向けて送信する。
vpnkit.exe
はhyperv-listen://00000000-0000-.../30D48B34-FACB-...
で待ち受けており、受け取ったデータを Decapsulation し、vpnkit.exe
プロセス内部に持っている仮想 L3 Switch へと送る。
vpnkit は、送信先毎に 仮想 TCP/IP endpoint を作成しており、これが Transport Layer ( L4 ) Proxy として TCP/UDP Flow を終端する。
内部 Switch はこの仮想 TCP/IP Endpoint に対し 1 つの Switch Port を接続しておき、送信先で判定し Filtering する。
もし知らない送信先が来た場合、新たに仮想 TCP/IP Endpoint が作られ、新しい Switch Port が作成 & 接続される。これらは全て
vpnkit.exe
プロセス内部で起こることで、Windows Host Kernel からはvpnkit.exe
が複数の相手と socket 通信しているようにしか見えない。● vpnkit-forwarder
Windows → LinuxKit で Port Forwarding を実現するサービス。
https://github.com/moby/vpnkit/blob/master/docs/ports.md
https://github.com/moby/vpnkit/tree/master/go/cmd/vpnkit-forwarder ( 元proxy-vsockd
)
https://github.com/moby/vpnkit/blob/master/go/cmd/vpnkit-userland-proxy ( 旧slirp-proxy
, 現vpnkit-expose-port
)
https://github.com/moby/vpnkit/tree/master/c/vpnkit-9pmount-vsock前準備
まずは前準備として、
vpnkit.exe
起動時に Port Forwarding 情報の共有のための 9p Server を立ち上げhyperv-listen://00000000-0000-.../0000F3A5-FACB-...
で待ち受ける。
Linuxkit 側では、onboot
時にvpnkit-9pmount-vsock
Container がvsock(cid=VMADDR_CID_HOST, Port=0000F3A5)
で接続し、その socket を Backend とした 9P filesystem を/port
に mount する。### `rfdno`, `wfdno` に設定されているのが、socket の file descriptor $ ctr --namespace services.linuxkit tasks exec --exec-id 1000 docker mount -l | grep /port # /port on /port type 9p (rw,relatime,sync,dirsync,trans=fd,dfltuid=1001,dfltgid=50,version=9p2000,msize=4096,rfdno=3,wfdno=3)Port Forwarding
それでは、実際に Port Forwarding されるまでの一連の処理を見ていく。
Container を立ち上げる。
PS> docker run -d -p 80:80 nginxDocker 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:80Port Forwarding 情報を受けた
vpnkit.exe
は、自身が Port Forwarding するその Port で Listen し始める。
ここで、Windows Host からlocalhost:80
にアクセスすると、まずvpnkit.exe
に connect され、vpnkit.exe
内で Multiplexing, Encapsulation されてhyperv-connect://<<DockerDesctopVM>>/0000F3A5-FACB-...
へ向けて送信される。
vpnkit-forwarder
がvsock(cid=VMADDR_CID_ANY, Port=0000F3A5)
で待ち受けており、受け取ったデータを Decapsulation, Demultiplexing し、後は Forward Proxy として[Dest IP]:80
にアクセスする。( ん、Dest IP 指定するなら
vpnkit-expose-port
の Listen 要らないのでは ? ここ とか ここ とか ここ 見ると Dest IP 教えてるっぽい )ちなみに 9p をわざわざ使っているのは、
vpnkit-expose-port
が起動中/port/XX:XX:XX:XX:XX:XX
File Descriptor をわざと Open したままにしておくことで、Crush や Kill された際に 9p のclunk
Message が vpnkit へ通知され、Leak を防ぐことができる為らしい。● Windows Named Pipe
Windows には、Named pipe ( 日本語で、名前付きパイプ ) と呼ばれるプロセス間通信の方法がある。
Unix にも同名の概念があるが、Windows の場合は以下の特徴がある。
- ファイル実体はなく、NPFS ( named pipe filesystem ) 上に mount される
\\.\pipe\PipeName
- 揮発性で、通信プロセスが止まれば消える
- Windows で Unix Domain Socket の代わりとして選択されるケースが多い
● \\.\pipe\docker_engine
com.docker.proxy
が Docker API Call を待ち受けている Named Pipe。
Docker Client が繋ぎに行っている。
\\.\pipe\docker_engine_windows
というのもあるが、こっちは Windows の dockerd へと繋がっている。PS> docker -H "npipe:////./pipe/docker_engine" info | wsl grep OSType # OSType: linux PS> docker -H "npipe:////./pipe/docker_engine_windows" info | wsl grep OSType # OSType: windows● \\.\pipe\dockerVpnKitControl
vpnkit.exe
起動時に、9p Control 用待受アドレスとして渡される 2 つのアドレスの内の 1 つ。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 を一旦リネームすると、新たに作り直されて解決する。
参考
- 投稿日:2019-03-18T09:30:00+09:00
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のコードリーディングをする際などに役立つはずです。
- 投稿日:2019-03-18T09:30:00+09:00
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のコードリーディングをする際などに役立つはずです。
- 投稿日:2019-03-18T07:42:05+09:00
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)
今までは、サーバー型モデルと呼ばれる、サーバーがテータを保持し提供する仕組みが一般的でした。
ですがブロックチェーンではP2Pと呼ばれる、各ピア(それぞれのデバイスのこと、ノードともいう)がデータを保持し、互いにデータの提供、要求を行う自律分散型の仕組みで成り立っています。ネットワークの種類
実際にブロックチェーンのアプリを開発する際には3種類のネットワークがあります。
メインネット
メインネットは実際の世界で使われているブロックチェーンです。
本番の環境ですので何かをやり取りする際には実際のETHが必要になります。
また、一度デプロイしてしまうとかき消すことはできません。テストネット
メインネットにデプロイする前に開発したものがきちんとどうだするか確認するための「実験場」のようなものです。
ここでのトークンは市場価値がありません。イーサリアムの代表的なテストネットは
・Rospoten
・Kovan
・Rinkebyだだ、テストネットもネットに繋がっており攻撃を受けることがあり、どのテストネット一長一短あります。
Ethereum の各種テストネット比較プライベートネット
構築者自身により構築されるネットワークのことで、個人や組織内で主に使います。
プライベートネットの構築
go-ethereumで構築する場合
Geth(Go Etherium)のプライベートネットワークの立ち上げ方法
規格のアップデートがあり仕様が変わってる部分があったなと感じたため
Gethを使ったプライベートネットの立ち上げはあまりお勧めではないです。4.go-ethereumへの接続
$ geth attach http://localhost:8545gethoを利用した場合
株式会社Popshoot提供する、プライベートネットをすぐに構築できるプラットフォームサービスを利用すれば簡単に構築することができます。
https://getho.io/終わりに
ブロックチェーンにネットワークの仕組みな関してある程度理解できました。
- 投稿日:2019-03-18T07:32:31+09:00
Digdag+Embulkで異種RDB間のデータ転送をローカルで検証できるようにする
最近社内でDigdag+Embulkでデータ連携をする機会が増えてきたので、ローカルで検証できる環境を作りました。
開発用のサーバがあればそれを使っても良いですが、気軽に試せるローカル環境があると何かと便利です。この記事ではDigdagとEmbulkについては詳しく説明しないので、基本的な使い方を知っている方向けです。
試すこと
実際こんなことをすることはほとんど無いと思いますが
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.lockDigdagから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_insertin: はデータの送信元からどんなクエリでデータを取得するかを記述します。今回は簡易的な例として 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を入れる必要があったりするため、色々と考えることはあります。
あくまで導入の第一歩としての紹介でした。
- 投稿日:2019-03-18T01:54:53+09:00
【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-networkWinNATに関する、より具体的な説明、制約事項等は下記記事にまとまっています。併せて読むと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」をクリックします。
その後、User Access Controlのポップアップが表示されるので、対象が間違っていないことを確認し、「Yes」を押します。
Power ShellがAdministrator権限で起動します。タイトルバーの左上に「Administrator:」と表示されていることを確認します。
新たにVirtual Switchを作成する(New-VMSwitch)
PowerShellから下記のコマンドを実行します。Virtual Switchの名前は何でもよいですが、ここでは「Hyper-V Internal with WinNAT」とします。Virtual Switchの作成には数分かかります。
- SwitchName: Hyper-V Internal with WinNAT (※任意に設定可能)
- SwitchType: Internal
New-VMSwitchPS 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-VMSwitchPS 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が生成されていることが確認できます。
当該Virtual Switchに接続するホストNICにIPアドレスを設定する
先ほどの当該Virtual Switchの作成に伴い、これに接続されるホストNICも併せて生成されています。Get-NetAdapterコマンドレットで、存在するNICの一覧を確認できますので、確認しておきます。
下記例では、「ifIndex」が「32」のものが該当します。この「ifIndex」の値は次に使うので控えておきます。Get-NetAdapterPS 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 GbpsNICにIPアドレスを付与するには、New-NetIPAddressというコマンドレットを利用します。対象のNICを指定するにあたり「ifIndex」の値を指定します。
なお、ここで設定するIPアドレスは、Hyper-V内のVMから見た場合にDefault Gatewayとなります。
- IPAddress: 192.168.254.1
- PrefixLength: 23
- InterfaceIndex: 32 (※Get-NetAdapterコマンドレットであらかじめ確認する)
New-NetIPAddressPS 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の設定を確認してみましょう。
WinNATを利用し、当該Virtual Switchから外部のネットワークへの通信をNATするよう設定する
最後に、今回作成したVirtual Switchに所属する(正確には、指定したIPアドレスレンジに所属する)ホストからのトラフィックをNATするための設定を追加します。
PowerShellからNew-NetNatコマンドレットを実行します。パラメーターは下記を指定しました。
- Name: HyperVinternalWinNAT (※WinNAT設定に対する名称。任意のものを指定可能。)
- InternalIPInterfaceAddressPrefix: NAT対象となるIPアドレスレンジ
New-NetNatPS 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.htmlBasic 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.htmlLimited 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/AmazonLinux2Vagrantfileの設定はまずはシンプルに下記のようにします。ホスト名は「stdsv3」とします。
VagrantfileVagrant.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 endAdministrator権限で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 eth0Internet上の適当な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用のネットワーク設定を自動化する
※続きます
- 投稿日:2019-03-18T01:54:53+09:00
【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-networkWinNATに関する、より具体的な説明、制約事項等は下記記事にまとまっています。併せて読むと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」をクリックします。
その後、User Access Controlのポップアップが表示されるので、対象が間違っていないことを確認し、「Yes」を押します。
Power ShellがAdministrator権限で起動します。タイトルバーの左上に「Administrator:」と表示されていることを確認します。
新たにVirtual Switchを作成する(New-VMSwitch)
PowerShellから下記のコマンドを実行します。Virtual Switchの名前は何でもよいですが、ここでは「Hyper-V Internal with WinNAT」とします。Virtual Switchの作成には数分かかります。
- SwitchName: Hyper-V Internal with WinNAT (※任意に設定可能)
- SwitchType: Internal
New-VMSwitchPS 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-VMSwitchPS 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が生成されていることが確認できます。
当該Virtual Switchに接続するホストNICにIPアドレスを設定する
先ほどの当該Virtual Switchの作成に伴い、これに接続されるホストNICも併せて生成されています。Get-NetAdapterコマンドレットで、存在するNICの一覧を確認できますので、確認しておきます。
下記例では、「ifIndex」が「32」のものが該当します。この「ifIndex」の値は次に使うので控えておきます。Get-NetAdapterPS 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 GbpsNICにIPアドレスを付与するには、New-NetIPAddressというコマンドレットを利用します。対象のNICを指定するにあたり「ifIndex」の値を指定します。
なお、ここで設定するIPアドレスは、Hyper-V内のVMから見た場合にDefault Gatewayとなります。
- IPAddress: 192.168.254.1
- PrefixLength: 23
- InterfaceIndex: 32 (※Get-NetAdapterコマンドレットであらかじめ確認する)
New-NetIPAddressPS 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の設定を確認してみましょう。
WinNATを利用し、当該Virtual Switchから外部のネットワークへの通信をNATするよう設定する
最後に、今回作成したVirtual Switchに所属する(正確には、指定したIPアドレスレンジに所属する)ホストからのトラフィックをNATするための設定を追加します。
PowerShellからNew-NetNatコマンドレットを実行します。パラメーターは下記を指定しました。
- Name: HyperVinternalWinNAT (※WinNAT設定に対する名称。任意のものを指定可能。)
- InternalIPInterfaceAddressPrefix: NAT対象となるIPアドレスレンジ
New-NetNatPS 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.htmlBasic 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.htmlLimited 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/AmazonLinux2Vagrantfileの設定はまずはシンプルに下記のようにします。ホスト名は「stdsv3」とします。
VagrantfileVagrant.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 endAdministrator権限で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 eth0Internet上の適当な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用のネットワーク設定を自動化する
※続きます
- 投稿日:2019-03-18T00:47:25+09:00
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"で再開するとループは見られなかった。
- 投稿日:2019-03-18T00:02:55+09:00
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 type
にBlue/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) でも使用できるように、ルールを追加します。
ルーティングの設定で、新しいターゲットグループを作成します。
名前は、Blue/Greenデプロイのためのターゲットグループであることがわかるように、stg-fargate-blue
としてます。Blue/Greenデプロイにおいて、ターゲットを切り替えるためのもう1つのターゲットグループは、ECSサービスの作成において作成するため、現時点ではターゲットグループは1つで大丈夫です。
「ステップ 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 Additional configuration
ここでは、Blue/Greenデプロイで使用する2つのターゲットグループに関する設定を行います。
このターゲットグループをCodeDeployが切り替えることによって、Blue/Greenデプロイが可能になります。ターゲットグループ1は、ALB作成時に作成した
stg-fargate-blue
を選択します。
ターゲットグループ2は、新規で作成します。名前は、
stg-fargate-green
としておきます。なお、ターゲットグループ名、ヘルスチェックパスについては環境に合わせて変更してください。
上記の設定が完了したら、ECSサービスを作成します。
この時点で、ALBを確認するとターゲットグループが作成されていることが確認できます。
CodeDeployについても確認してみると、アプリケーションとデプロイグループが作成されています。(この例だと名前が微妙ですね。)
デプロイグループのデプロイ設定を変更
デプロイグループのデフォルトの設定では、デプロイされた際に、新しくデプロイが行われた側のターゲットグループに、トラフィックが自動で流れ始めてしまいます。
今回は、動作確認をしてからターゲットグループの変更を行いたいため、トラフィックの再ルーティングするタイミングを指定します。今回は15分後に、再ルーティングされるように変更します。この設定をすることで、15分間待たなくても手動で再ルーティングすることも可能です。詳細は後述。また、デフォルトの設定では、タスクの正常な展開後(再ルーティング後)1時間待ってから元のタスクが終了します。今回は30分後にタスクが終了するように設定を変更します。
デプロイグループを選択し、「編集」ボタンを押下することで、デプロイ設定画面が表示されます。
Blue/Greenデプロイをする
ECRのリポジトリに新しいDockerイメージをPushする
タグ
1.0.2
,latest
をつけたDockerイメージをECRのリポジトリにpushします。
latest
が最新のイメージのみについています。古いイメージについていたlatest
は自動的に剥がされています。
タスク定義のリビジョンを作成し、ECSサービスを更新する
タスクの定義の新しいリビジョンを作成し、ECSサービスを更新します。
このサービスを更新した時点で、CodeDeployによるデプロイが開始されます。CodeDeployからデプロイのステータス、トラフィック移行の進行状況を確認できます。
タスクを確認すると、新旧のタスクのリビジョンが起動していることを確認できます。
ALBのエンドポイントの8080番ポートにアクセスし、正常に動作していることを確認します。
問題がなければ、デプロイしたターゲットグループにトラフィックを向け、リリースを実施します。CodeDeployの「トラフィックの再ルーティング」ボタンを押下することで、デプロイしたターゲットグループにトラフィックが流れ始めます。
ALBのリスナーを確認すると、ターゲットグループが変更されていることを確認できます。
HTTPSの転送先のターゲットグループがstg-fargate-blue
からstg-fargate-green
に変更されています。
旧タスクの待機時間が終了すると、旧タスクが停止されデプロイが完了します。
ロールバック機能
デプロイに問題が発生した場合は、展開を停止してロールバックすることができます。
サービスのデプロイ
タブに表示されている「Stop and rollback deployment」ボタンを押下することで、展開が停止されます。まとめ
ECSでCodeDeployを使用したBlue/Greenデプロイメントを利用することで、簡単にBlue/Greenデプロイができました。CodePipelineを利用したデプロイもサポートされているので、さらにデプロイが簡単になりますね!
参考記事
参考にさせていただきました。ありがとうございます。
AWS CodeDeploy による AWS Fargate と Amazon ECS でのBlue/Greenデプロイメントの実装
ECSでCodeDeployを使用したBlue/Green Deploymentがサポートされたので早速試してみた #reinvent | DevelopersIO