20190318のLinuxに関する記事は8件です。

LinuxPCで使うiscsiのパフォーマンスはどれくらい?

この記事のまとめ

image.pnglinux-pcで使う「iSCSI」ってどれぐらい早い?*
 ⇒ 適当な 第3世代 Corei7 PC で 71.5 MB/s(572Mbps)でました。
   ローカルディスクの速度と比べて
    書き込み速度は55%程度 
    読み込み速度は83%程度

リソースどれぐらい消費するの?
 ⇒ Initiator 側のCPU使用率は、ローカルDisk使用時と大して変わりませんでした。
 ⇒ target 側のCPU使用率は 書き込み時に50%くらいまで上がりました。
 ⇒ ネットワーク帯域は iSCSIの転送速度に対して 105% 程度でした。

実験構成

image.png
Debian 9.6 がインストールされたPCをGigabit-Ethernetでつないだ単純な構成。
スイッチも4千円以下で買えるBUFFALO の Gigabit Switching-HUBを使用。

結果サマリ

ローカル iSCSI
4GB Sequential Write 127 MB/s 71.5 MB/s
4GB Sequential Read 136 MB/s 113 MB/s

書き込み速度は55%程度 読み込み速度は83%程度 を記録しました。

iSCSI書き込み時の Initiator / target の CPU使用率 , Ethernetの利用帯域

  • Initiator のCPU使用率はローカル書き込み時に比べてさほど変化がありませんでした。むしろiowait timeが減ることで、見かけ上のCPU使用率はiSCSIを使用していたほうが低いです。
  • 今回のtarget側PCが非力なCPUだったせいもあり、target側PCのCPU使用率は50%程に達しています。softirq,system Timeが支配的です。
  • ネットワークトラフィックは 600 Mbps 程度に達しています。実際のiSCSIの書き込み速度71.5 MB/s(572Mbps)に対して 105% 程度の割合です。 image.png

ローカル書き込み時の Initiator / target の CPU使用率(比較用)

image.png

スペック詳細

iSCSI Initiator iSCSI Target
kernel 4.9.0-8-amd64 4.9.0-8-amd64
CPU Intel Core i7-3770K 3.50GHz Intel Celeron 450 2.20GHz
MEM 8GB 2GB
HDD ST6000DM003 5400rpm ST6000DM003 5400rpm
NIC 1000Base-T 1000Base-T
+chip RTL8168evl/8111evl RTL8168c/8111c
+Driver r8169 2.3LK-NAPI r8169 2.3LK-NAPI

ethtool -k で NICのオフロード機能の状態を調べてみたところ、どちらも以下のみONでした。

  • rx-checksumming: on
  • generic-receive-offload: on
  • rx-vlan-offload: on
  • tx-vlan-offload: on
  • highdma: on [fixed]

測定方法

# 4GB Sequential Write
# dd で 4GBのファイルを書き込み。oflag=directで書き込みキャッシュを利用しない。
chinachu@debian:~$ dd if=/dev/zero of=/mnt/tmp.data bs=4M count=1024 oflag=direct
1024+0 records in
1024+0 records out
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 33.8208 s, 127 MB/s


# 4GB Sequential Read
#先ほど作ったファイルをそのまま読み込む。
chinachu@debian:~$ dd if=/mnt2/tmp.data of=/dev/null bs=4M count=1024 
1024+0 records in
1024+0 records out
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 38.1206 s, 113 MB/s


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

PL-PS data transfer using AXI-DMA on Zybo-Z7-20 (Xilinx tool set 2018.3, Linux)

Requirement

  • Digilent Zybo-Z7-20 board
  • Xilinx tool set 2018 (Vivado, XSDK)
  • Linux on Zybo-Z7-20 (See previous articles (a) and (b))

Create AXI-DMA core in Vivado project

First, We need to create a Vivado project for Zybo-Z7-20 with axi-dma core.
The detailed process is explained here in the good article, "Using the AXI DMA in Vivado" with step by step screenshots. Because we execute DMA operation from Linux application, we only need to follow the article to the end of the section "Export the hardware design to SDK".
After exporting hardware, according to my previous articles, we need to create device tree sources using XSDK. The dts files XSDK automatically created are to be revised a bit.

Modify Device Tree Source

Basically, we need to modify dts files required to boot Linux as in previous articles. Here we see the additional modifications required to setup AXI-DMA.
In system-top.dts, we will see the new #include description comparing to that in the previous article. The #include should be,

system_top.dts
/include/ "pl.dtsi"

In "pl.dtsi", we will find the node for axi_dma_0 which specify the parameters passed to the driver. But the Xilinx driver for axi_dma does not have interfaces for user application. That's why we need to install xilinx_axidma driver as the interface driver. As in the README, 'xilinx_axidma' requires following node under amba_pl node.

pl.dtsi
        axidma_chrdev: axidma_chrdev@0 {
            compatible = "xlnx,axidma-chrdev";
            dmas = <&axi_dma_0 0 &axi_dma_0 1>;
            dma-names = "tx_channel", "rx_channel";
        };

The parameter dmas is specifying the pair of the dma-channel with their IDs. Though these IDs are the values of xlnx,device-id in both dma-channel nodes, auto-generator sets the two values to 0.

pl.dtsi
            dma-channel@40400000 {
                compatible = "xlnx,axi-dma-mm2s-channel";
                dma-channels = <0x1>;
                interrupts = <0 29 4>;
                xlnx,datawidth = <0x20>;
                xlnx,device-id = <0x0>;
            };
            dma-channel@40400030 {
                compatible = "xlnx,axi-dma-s2mm-channel";
                dma-channels = <0x1>;
                interrupts = <0 30 4>;
                xlnx,datawidth = <0x20>;
                xlnx,device-id = <0x0>;
            };

We need to revise xlnx,device-id in later dma-channel as 1

pl.dtsi
            dma-channel@40400030 {
                compatible = "xlnx,axi-dma-s2mm-channel";
                dma-channels = <0x1>;
                interrupts = <0 30 4>;
                xlnx,datawidth = <0x20>;
                xlnx,device-id = <0x1>;
            };

Compile the device tree source, and move the created dtb file to the boot disk.

Build Interface driver (xilinx_axidma)

Clone the repository xilinx_axidma,

$ git clone https://github.com/bperez77/xilinx_axidma

We need to define the values CROSS_COMPILE, ARCH, and KBUILD_DIR. Our cross compile environment, we only need to source env.sh in the previous articles. After setting up cross compile environment, we type simply make at the root of xilinx_axidma.

$ cd xilinx_axidma
$ make 

Following files under xilinx_axidma should be moved to rootfs on Zybo-Z7-20.
- driver/axidma.ko
- library/libaxidma.so
- examples/axidma_transfer

Driver installation

Note that axidma.ko can only be used from root account, the driver, can be installed simply with

$ insmod axidma.ko

If the axi-dma core is properly installed, following messages can be seen in dmesg.

$ dmesg | grep "axidma"
[   25.223219] axidma: loading out-of-tree module taints kernel.
[   25.225089] axidma: axidma_dma.c: axidma_dma_init: 718: DMA: Found 1 transmit channels and 1 receive channels.
[   25.225100] axidma: axidma_dma.c: axidma_dma_init: 720: VDMA: Found 0 transmit channels and 0 receive channels.

And device file axidma is found in /dev.

$ ls /dev/axidma -la
crw------- 1 root root 245, 0 Mar 18 02:16 /dev/axidma

The driver uses read, write interfaces of char device to transfer data between user and kernel region. The detailed programming interface can easily be understood from the source code of the example axidma_transfer.
axidma_transfer takes two arguments: one is the input file to be transferred to PL, and another is output file the data came from PL to be written. For example,

$ axidma_transfer test.dtb test2.dtb
AXI DMA File Transfer Info:
        Transmit Channel: 0
        Receive Channel: 1
        Input File Size: 0.01 MiB
        Output File Size: 0.01 MiB

Writing output data to `test2.dtb`.

In our design, rx and tx channels are connected via FIFO, the contents of the file test.dtb is transferred to the FIFO in PL. Immediately, the data is transferred back and stored as the file test2.dtb

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

俺のオススメする監視ツール5選!!

監視ツール

監視ツールと一概に言っても色々種類あるのですが、今回はコマンド等で手軽に実行できるリソース監視ツールを紹介します。
一部、あまりオススメできないのもありますので、デメリット等よく読んでから利用してください。

  1. top
  2. htop
  3. ctop
  4. gtop
  5. conky

top

様々なOSにデフォルトで入っているかと思います。多分。

## 起動方法
$ top

ubuntu-top.png
↑こんな感じになります。

  • メリット
    • どのOSにも入っている(多分)
  • デメリット
    • macだとtopコマンド自体がリソースを喰う。windowsは未確認です
    • ↑僕のPCだと8〜15%ほどCPU消費してました

htop

topコマンドの強いバージョン。
こちらで紹介されているように、top使うくらいならhtop使いましょう。ってくらい良い感じのツールです。

## インストール方法
$ sudo yum install htop

## 起動方法
$ htop

htop.png

  • メリット
    • 導入が簡単
    • 見た目もよし
  • デメリット
    • 好きすぎて辛い

ctop

Dockerコンテナのリソース消費量を確認するのが楽。
稼働中のコンテナのリソース、選択した単体のコンテナの詳細リソースまでを表示してくれます。導入が楽なのもポイント高い。

## インストール方法
$ sudo wget https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-linux-amd64 -O /usr/local/bin/ctop
$ sudo chmod +x /usr/local/bin/ctop

## インストール方法(Macの場合)
$ brew install ctop


## 起動方法
$ ctop

ctop.png
↑コンテナ一覧(1つしかないけど)

ctop_2.png
↑対象コンテナをEnterで選択で詳細モード

  • メリット
    • 手間をかけずに導入できる
    • brewでmacにインストールできる ←ご指摘いただきありがとうございます!
  • デメリット
    • Linux用のバイナリなのでMacでは動作しない。 docker stats などを使いましょう

gtop

グラフィカルな感じにリソース監視等ができます。
Node.JSで動作してるようなので自分好みにカスタムできそうです。

## インストール方法
$ npm install gtop -g

## 起動方法
$ gtop

gtop.png
↑グラフィカルな感じがいい感じ

  • メリット
    • Nodeが入ってたら手軽に導入できる
    • グラフィカルな感じがいい感じ
  • デメリット
    • Nodeが入ってる前提なので入ってない場合導入ダルい

カスタム・自作方法はこちらを参考にしてみてください。

conky

ラズパイとかUbuntuで動かしてました。
導入方法とかは忘れたのですが、自分好みにカスタマイズできるので、興味ある方は是非。

https://github.com/brndnmtthws/conky

まとめ

監視ツールって導入が大変だったり、設定が大変だったりしますが、単体のリソース監視をするのであれば上記で紹介したツールで十分ですかね。

個人的にはhtopctopが好きですが、gtopもなかなか可能性を秘めているので今後、うまく活用できれば良いかなと思います。

それとスタバでhtopがカッコ良かったのも2年前の話ですね。
今風に行くならscreenコマンドで画面分割しながら、Node.JS, blessed-contribを利用して自作したリソース監視ツールを起動させつつDockerいじり倒すとか。今風がなんなのかわからないですが。

おすすめ♪ リンク♪

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

インフラの自動テストを実現するツール「Serverspec」

インフラのテスト自動化を実現するツール「Serverspec」

login001

1.自動テストツール

1.1. 目的

【自動テストツール】を導入する目的は、DevOpsの一環として、テスト工程の自動化も視野に入れ自動化により運用の効率化を図る目的と考えて調査・検証した結果を以下の記事を記載します。

1.2. 自動テストツールとは?

度重なるCIによって都度同じテストを人手でするのは、あまりにも非効率ですよね?
決まり切ったテストなら自動化し【効率性】を上げ生産性を向上すると共に、テストの人為的なミスを削減し【品質向上】が可能な【テスト自動化ツール】を導入するべきでしょう。

テスト自動化研究会【テスト自動化の8原則】

以上を読んでいただけると分かると思いますが、自動化したテストに関しては人手が必要なくなるだけで、結果を確認する工程やテスト内容を考えるのは人手は必要です。

また、手動テストもなくならないことを踏まえたうえで自動テストツールを導入する必要があります。

既に自動テストツールを導入しているプロジェクトの殆どが、事の大小はあっても問題を抱えていることを認識したうえで導入の検討をして下さい。

1.3. 自動化すべきテスト項目の条件

まず、全てのテストが自動化すればいいのではありません。

自動化したことにより、テスト結果が望む結果であったとしても他要因でNGとなるケースを見落とすことになります。(例えばWebサービスで画面遷移に関わるテストを自動実行し、画面遷移自体は想定通りになったとしてもレイアウトや画像が間違って変わっていてもツールでは気付けない等)

自動化したほうが品質の良いテストを実施でき効率化するためにツールであることを認識したうえでテスト項目を選定することを考える。

また、文字列や数字などのテキストデータの判別は自動テストツールで容易にチェックできますが、画像やランダム性があるものの確認は自動テストツールでは実現は難しく、できたとしても品質担保を説得力のある証拠で提示するのが難しいため、手動でのテストとするべきです。

以上より自動化すべきテスト項目の条件を以下とします。

  • CIの度に実施すべきテスト。(同じテストを何度も繰り返し実行する内容)

  • 既にテストコードがあり、そのテストコードで品質担保が可能であること。

  • テストの確認内容が自動化ツールで全て網羅できる範疇であること。

  • CIの度にテストコードを改修しなくてもよいこと。改修したとしても僅かであり影響範囲が小さいこと。

1.4. テスト自動化ツール選定

テスト自動化ツールも用途により多種多様です。そのため、プロジェクトに合ったツールを選定することから始める必要があります。

Webアプリケーションテスト自動化ツール「Selenium」

対象:Webアプリケーション
動作環境:ブラウザ拡張、クライアントサーバーモデル
特徴:ブラウザ上で動作するWebアプリのテストを自動化するツールです。キャプチャー・リプレイ機能では、実際に行ったテストを再現することも可能です。

「Selenium IDE」により、プログラミング言語の知識が少ない人でも、簡単にテストをできるようにブラウザ操作を記録してテストスクリプトを作成する機能も備わっています。

Webアプリケーションテスト自動化ツール「Jenkins」

対象:Webアプリケーション
動作環境:Windows、UNIX、Linux、MacOS等
特徴:継続的インテグレーション(CI)ツールとして有名ですが、Jenkinsにテストを自動的に行うプラグインを紐づけることにより、スケジューラー機能による定期的なテスト実行が可能になります。

Javaアプリケーション単体テスト自動化ツール「JUnit」

対象:Javaモジュール
動作環境:Linux
特徴:Javaのテストを小さな単位でテスト可能。

ITインフラテスト自動化ツール「Serverspec」

対象:サーバの設定値やパッケージがインストール確認、特定サービスの状態確認、指定ポートでの通信のテストを自動化できる。
動作環境:
特徴:サーバの設定、状態が想定通りになっているかチェックできる。
サーバにエージェントを導入する必要性がない。

他にも

スマートフォンアプリテスト自動化ツール「Appium」

Windowsアプリケーションテスト自動化ツール「QCWing」

負荷テスト自動化ツール「Apache Jmeter」

と試験内容に応じて色々な製品が存在します。

1.5. 自動テストツール選定

私はITインフラ開発者なので【Serverspec】を
今までのExcelで書かれたパラメータシートを実機をにらめっこして、ITインフラの単体試験としていた現場が多いと思われます。それを自動化することで他の作業に工数を割くことができればより開発を促進し信頼性の高いものを提供することが可能となります。

1.6. Serverspecとは?

Serverspec(サーバスペック)とは、サーバ状態のテスト自動化フレームワークです。UNIX/LinuxサーバとWindowsサーバに対応します。
構築したサーバ環境が意図した通り構成されているか自動的に確認作業を実施できるツールです。
Serverspecは、Rubyで実装されています。
構成管理ツール(Ansibleなど)で自動築したサーバを想定通りの設定及びあるべき姿であることの確認までを繰り返しテストを行い、【常にあるべき姿を維持しているか】をチェックできます。

Serverspecが導入されたサーバからテストを実施するためは、エージェントは必要なく、LinuxであればSSH接続、WindowsサーバはWinRM接続可能な環境であれば実現可能となっているため、手軽に組み込むことも可能となっている。

2. Serverspecインストール

参考にしたサイト

Serverspecでテスト自動化 - IDCF テックブログ

「Serverspec」を使ってサーバー環境を自動テストしよう | さくらのナレッジ

大規模サーバ更改でServerspecを使ってみました - Taste of Tech Topics

2.1. 検証環境

OS:CentOS 7.5
ミドルウェア:serverspec(2.41.3),rake(12.3.2),bundler(2.0.1),winrm(2.3.1)
前提ミドルウェア:ruby(2.3.8)、openssl-devel(1.0.2k-16)、readline-devel(l-6.2-10)、zlib-devel(1.2.7-18)

2.2. Ruby2.3.8インストール

# yum install -y openssl-devel readline-devel zlib-devel
# cd /root
# git clone git://github.com/sstephenson/rbenv.git .rbenv
# cd .rbenv
# mkdir shims versions plugins
# cd plugins/
# git clone https://github.com/sstephenson/ruby-build.git ruby-build
# git clone git://github.com/sstephenson/rbenv-default-gems.git rbenv-default-gems
# vi ~/.rbenv/default-gems
bundler
rbenv-rehash

# vi ~/.bashrc
export RBENV_ROOT="/root/.rbenv"
export PATH="$PATH:$RBENV_ROOT/bin"
eval "$(rbenv init -)"

# source ~/.bashrc
# cd /root
# rbenv install -l   #※1

# rbenv install 2.3.8
# rbenv global 2.3.8
# ruby -v

※1:rbenv install -lrbenv: no such command installとメッセージが出力される場合は既に古いrubyを導入してため、以下コマンドで回避することができます。

# .rbenv/plugins/ruby-build/install.sh

2.2. Serverspecインストール

# gem install bundler※1
# bundle init       # GemfileとGemfile.lockが作成されます。
# vi Gemfile
# 以下を追記
gem "serverspec"
gem "rake"
gem "winrm"     #windowsへのテストを実施する場合にインストール
# bundle install --path ./   # bundlerによりGemfileに書かれたgemをインストールする

※1:bundlerに関しては、bundler、bundle execについてを参照。

2.3. Serverspec初期設定

Serverspecの初期設定をするためserverspec-initコマンドを実行し、質問に応答します。

# bundle exec serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1  # serverspecのクライアントのOSタイプの選択でUNIXである"1"を選択

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 1  # serverspecのクライアントの接続方式の選択でSSHである"1"を選択

Vagrant instance y/n: n # Vagrantは、私の環境では未使用のため"n"を選択
Input target host name: web01 # クライアントのホスト名を入力 ※1
 + spec/
 + spec/web01/
 + spec/web01/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

※1:クライアントを入力するとサンプルのテストコードが作成されます。

2.4. 標準的な構成

テスト実行コマンドrakeは、Rakefileを最初に読み込み、対象となるspecディレクトリ配下のテストコードファイル(*_spec.rd)を実行します。

テスト対象のサーバを複数台とする場合は、標準的な構成すると以下のような構成となります。

 ├─ Rakefile
 ├─ Gemfile
 ├─ Gemfile.lock
 ├─ .rspec
 ├─ ruby
 │  └─ 2.3.8
 │
 └─ spec/
    ├─ web01/
    │  ├ base_spec.rb
    │  └ httpd_spec.rb
    ├─ web02/
    │  ├ base_spec.rb
    │  └ httpd_spec.rb
    ├─ app01/
    │  ├ base_spec.rb
    │  └ tomcat_spec.rb
    ├─ spec/db01/
    │  ├ base_spec.rb
    │  └ mysql_spec.rb
    └─ spec_helper.rb

テストを個々に実行する場合は、rakeコマンドの引数にspec:<サーバアドレス>を指定する。

# bundle exec rake spec:web01

だが、この構成だとサーバが増える度にサーバ毎のフォルダを作成し、同じ種別のサーバのテストコードファイルをコピーしなければならない。また、テスト実行も全てもしくはサーバ単位となることに留意すること。

3. role

Serverspecを運用するにあたり、標準構成では、上記理由によりサーバ台数規模が膨大になると管理が大変になるため、roleによるサーバ種別毎のテスト内容を分けたいと思います。

role構成については、Serverspec用のspec_helperとRakefileのサンプルをひとつを参考にさせていただきました。

3.1. role構成

role構成するにあたり、以下のようなディレクトリ、ファイル構成とします。

├── Gemfile
├── Gemfile.lock
├── Rakefile
├── common_spec/        # 他プロジェクトと共通するspecのサブモジュール
│   ├── common/
│   ├── group/
│   ├── user/
│   ├── kernel/
│   ├── firewall/
│   ├── rpm/
│   └── zabbix-agent/
├── ferture_spec/       # プロジェクト特有のspecのサブモジュール
│   ├── group/
│   ├── user/
│   ├── kernel/
│   ├── firewall/
│   ├── rpm/
│   ├── service/
│   ├── cron/
│   ├── logrotate/
│   ├── zabbix-agent/
│   ├── httpd/
│   ├── tomcat/
│   └── mysql/
├── host_vars/          # ホスト特有の値をテストしたい場合に使用
│   ├── dbsvr01.yml
│   └── win-manage01.yml
├── audit
│   └── json/               # テスト証跡
├── hosts_production.yml    # 本番環境のホストとrole一覧
├── hosts_staging.yml       # 開発環境のホストとrole一覧
└── spec/               # helper他。
    └── spec_helper.rb

role構成に対応するため、ディレクトリを作っていきます。

mkdir -m 755 ./common_spec
mkdir -m 755 ./common_spec/common/
mkdir -m 755 ./common_spec/group/
mkdir -m 755 ./common_spec/group/system
mkdir -m 755 ./common_spec/user/
mkdir -m 755 ./common_spec/user/system
mkdir -m 755 ./common_spec/kernel/
mkdir -m 755 ./common_spec/kernel/system
mkdir -m 755 ./ferture_spec/user/web
mkdir -m 755 ./ferture_spec/user/db
mkdir -m 755 ./ferture_spec/user/ap
mkdir -m 755 ./ferture_spec/kernel/
mkdir -m 755 ./ferture_spec/kernel/web
mkdir -m 755 ./ferture_spec/kernel/db
mkdir -m 755 ./ferture_spec/kernel/ap
mkdir -m 755 ./ferture_spec/firewall/
mkdir -m 755 ./ferture_spec/firewall/web
mkdir -m 755 ./ferture_spec/firewall/db
mkdir -m 755 ./ferture_spec/firewall/ap
mkdir -m 755 ./ferture_spec/rpm/
mkdir -m 755 ./ferture_spec/rpm/web
mkdir -m 755 ./ferture_spec/rpm/db
mkdir -m 755 ./ferture_spec/rpm/ap
mkdir -m 755 ./ferture_spec/service/
mkdir -m 755 ./ferture_spec/service/web
mkdir -m 755 ./ferture_spec/service/db
mkdir -m 755 ./ferture_spec/service/ap
mkdir -m 755 ./ferture_spec/cron/
mkdir -m 755 ./ferture_spec/cron/web
mkdir -m 755 ./ferture_spec/cron/db
mkdir -m 755 ./ferture_spec/cron/ap
mkdir -m 755 ./ferture_spec/logrotate/
mkdir -m 755 ./ferture_spec/logrotate/web
mkdir -m 755 ./ferture_spec/logrotate/db
mkdir -m 755 ./ferture_spec/logrotate/ap
mkdir -m 755 ./ferture_spec/zabbix-agent/
mkdir -m 755 ./ferture_spec/zabbix-agent/staging
mkdir -m 755 ./ferture_spec/zabbix-agent/production
mkdir -m 755 ./ferture_spec/httpd/
mkdir -m 755 ./ferture_spec/httpd/staging
mkdir -m 755 ./ferture_spec/httpd/production
mkdir -m 755 ./host_vars
mkdir -m 755 ./audit
mkdir -m 755 ./audit/json

3.1.1 アドレス、roleファイル

まずは、環境毎(hosts_production、hosts_staging)にアドレス及びroleを記載するファイルをyamlファイルを分ける方針としています。

また、hosts配下にターゲットとなるアドレス(IPアドレス、名前解決できる環境であればホスト名)を設定し、roles配下で実行するtaskのディレクトリを設定します。

各タスクのディレクトリ配下に実施にテストを実施する内容を[*_spec.rb]に設定する。

例えばuserテスト定義ファイルの場合、[common_spec/user/system/user_spec.rb]、[ferture_spec/user/web/user_spec.rb]、[ferture_spec/user/db/user_spec.rb]、[ferture_spec/user/ap/user_spec.rb]と4つ保持しているが、テスト実行コマンドの引数でwebと指定した場合は、roleの設定で[common_spec/user/system/user_spec.rb]、[ferture_spec/user/web/user_spec.rb]だけをテストする仕組みとなっています。

また、環境毎(hosts_production、hosts_staging)に設定内容に差異が存在するようなテストをroleで吸収するため、[zabbix-agent/staging/zabbix-agent_spec.rb]、[zabbix-agent/production/zabbix-agent_spec.rb]と分けroleで指定環境のテストのみを実施する仕組みとなっています。

hosts_staging.yml

---
shared_settings:
  :ssh_opts:
    :user: operator
    :keys: /home/operator/.ssh/id_rsa
    :port: 22
web:
  :hosts:
    - web01.sdomain.com
    - web02.sdomain.com
  :roles:
    - common
    - group/system
    - user/system
    - user/web
    - kernel/system
    - kernel/web
    - firewall/web
    - rpm/web
    - service/web
    - cron/web
    - logrotate/web
    - zabbix-agent/staging
    - httpd/staging
db:
  :hosts:
    - db01.sdomain.com
  :roles:
    - common
    - group/system
    - user/system
    - user/db
    - kernel/system
    - kernel/db
    - firewall/db
    - rpm/db
    - service/db
    - cron/db
    - logrotate/db
    - zabbix-agent/staging
    - mysql/staging
app:
  :hosts:
    - ap01.sdomain.com
  :roles:
    - common
    - group/system
    - user/system
    - user/ap
    - kernel/system
    - kernel/ap
    - firewall/ap
    - rpm/ap
    - service/ap
    - cron/ap
    - logrotate/ap
    - zabbix-agent/staging
    - tomcat/staging

hosts_production.yml

---
shared_settings:
  :ssh_opts:
    :user: operator
    :keys: /home/operator/.ssh/id_rsa
    :port: 22
web:
  :hosts:
    - web01.domain.com
    - web02.domain.com
    - web03.domain.com
    - web04.domain.com
  :roles:
    - common
    - group/system
    - user/system
    - user/web
    - kernel/system
    - kernel/web
    - firewall/web
    - rpm/web
    - service/web
    - cron/web
    - logrotate/web
    - zabbix-agent/production
    - httpd/production
db:
  :hosts:
    - db01.domain.com
    - db02.domain.com
  :roles:
    - common
    - group/system
    - user/system
    - user/db
    - kernel/system
    - kernel/db
    - firewall/db
    - rpm/db
    - service/db
    - cron/db
    - logrotate/db
    - zabbix-agent/production
    - mysql/production
app:
  :hosts:
    - ap01.domain.com
    - ap02.domain.com
  :roles:
    - common
    - group/system
    - user/system
    - user/ap
    - kernel/system
    - kernel/ap
    - firewall/ap
    - rpm/ap
    - service/ap
    - cron/ap
    - logrotate/ap
    - zabbix-agent/production
    - tomcat/production

3.1.2. Rakefile

【標準Rakefile】

require 'rake'
require 'rspec/core/rake_task'

task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  targets = []
  Dir.glob('./spec/*').each do |dir|
    next unless File.directory?(dir)
    target = File.basename(dir)
    target = "_#{target}" if target == "default"
    targets << target
  end

  task :all     => targets
  task :default => :all

  targets.each do |target|
    original_target = target == "_default" ? target[1..-1] : target
    desc "Run serverspec tests to #{original_target}"
    RSpec::Core::RakeTask.new(target.to_sym) do |t|
      ENV['TARGET_HOST'] = original_target
      t.pattern = "spec/#{original_target}/*_spec.rb"
    end
  end
end

標準Rakefileでは、引数によって対象サーバや実行内容を分ける構成となるrole対応できないためrole対応できるように書き換えます。

【role対応Rakefile】

require 'rake'
require 'yaml'
require 'rspec/core/rake_task'

## 環境変数 SPEC_ENV で環境名を指定。
spec_env = ENV['SPEC_ENV']
if spec_env
  path_candidate = File.expand_path("../hosts_#{spec_env}.yml", __FILE__)
  if File.exists?(path_candidate)
    hosts_defined = path_candidate
  else
    raise RuntimeError, "\n======\nERROR: No hosts defined for #{spec_env}.\n======"
  end
else
  ## SPEC_ENV が省略されたら終了

  end
end


## 環境名に対応する定義ファイルを読む
properties = YAML.load_file(hosts_defined)

task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  ## 定義ファイルから spec:大分類:ホスト名を全部作成する。spec:all 用
  all_tasks = properties.each_pair.map { |key, values|
    ## 共通設定はホスト扱いしない。
    next if key == 'shared_settings'
    values[:hosts].map {|host| 'spec:' + key + ':' + host }
  }.flatten.compact

  ## 全部実行するタスク (spec:all)
  desc "all target for #{spec_env}"
  task :all => all_tasks

  ## ホスト定義をまわす、大分類はmaster_rollって名前で扱う
  properties.each_pair do |master_roll, entries|
    ## 共通設定は大分類扱いしない(spec_helperで使う)
    next if master_roll == 'shared_settings'

    ## 大分類に割り当てられているroleを抽出する
    role_pattern = entries[:roles].join(',')

    namespace master_roll.to_sym do
      hosts = entries[:hosts]

      ## 大分類別に全ホスト実行するタスク (spec:大分類:all)
      desc "all target of #{master_roll} for #{role_pattern}"
      task :all => hosts.map {|h| 'spec:' + master_roll + ':' + h }

      ## 大分類別に個別ホスト実行するタスクを定義する (spec:大分類:ホスト名)
      hosts.each do |host|
        desc "Run serverspec tests to #{master_roll}: #{host} for #{role_pattern}"
        RSpec::Core::RakeTask.new(host.to_sym) do |t|
          ## どれかがこけても途中でやめない。
          t.fail_on_error = false
          ENV['TARGET_HOST'] = host
          ENV['SPEC_ENV'] = spec_env

          ## specとcommon_specとferture_specをざっくり取って、定義ファイル上のロールに対応するspecを読み込ませる。
          t.pattern = "{spec,common_spec,ferture_spec}/{#{role_pattern}}/**/*_spec.rb"
          t.rspec_opts = "--format json -o audit/json/#{host}.json" #jsonでログ出力
        end
      end
    end
  end
end

3.1.3. helperファイル

各クライアントの接続方式やsshの環境別オプション、dockerにも対応など環境などに合わせるため、spec_helperを修正します。

require 'serverspec'
require "docker"
require 'net/ssh'
require 'yaml'

case ENV['SPEC_BACKEND']
## 環境変数 SPEC_BACKEND がdocker|DOCKERだったらSSHじゃなくてDockerバックエンドを使う。
when "DOCKER", 'docker'
  set :backend, :docker
  set :docker_url, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
  ## Dockerでためす場合、DOCKER_IMAGEを指定する。
  set :docker_image, ENV['DOCKER_IMAGE']
  set :docker_container_create_options, {'Cmd' => ['/bin/sh']}
  Excon.defaults[:ssl_verify_peer] = false
else
  ## デフォルトのバックエンドはSSH
  set :backend, :ssh
  set :request_pty, true

  ## このへんはRakeと一緒、定義ファイルを決定
  spec_env = ENV['SPEC_ENV']
  if spec_env
    path_candidate = File.expand_path("../../hosts_#{spec_env}", __FILE__)
    puts path_candidate
    if File.exists?(path_candidate)
      hosts_defined = path_candidate
    else
      raise RuntimeError, "\n======\nERROR: No hosts defined for #{spec_env}.\n======"
    end
  else
    hosts_defined = File.expand_path("../../hosts_staging", __FILE__)
  end


  ## spec_helperでもRakefile同様にホスト定義を読み込む
  properties = YAML.load_file(hosts_defined)

  host = ENV['TARGET_HOST']
  mainrole = properties.select {|k,v| v[:hosts].include?(host) if v[:hosts] }.keys.first

  ## ホスト固有の値を書いたファイルがあればつかう。
  host_vars = YAML.load_file(
    File.expand_path("../../host_vars/#{host}.yml", __FILE__)
  ) if File.exists?(File.expand_path("../../host_vars/#{host}.yml", __FILE__))

  spec_property =  properties[mainrole]
  spec_property[:host_vars] =  host_vars ||= {}

  ## 環境変数DEBUGがあったらset_propertyに渡される値を表示する
  puts spec_property.to_yaml if ENV['DEBUG']

  set_property spec_property

  ## specの中で大分類を使うかもしれないと思ってとりあえず環境変数に突っ込んである。
  ENV['SPEC_MAINROLE'] = mainrole

  ## 環境別SSH接続設定をマージしていく
  options = Net::SSH::Config.for(host).merge(properties['shared_settings'][:ssh_opts])

  ### 大分類の下にもssh_optsがあったらそっちを優先で上書き
  options.merge!(properties[mainrole][:ssh_opts]) if properties[mainrole][:ssh_opts]
  options[:user] ||= 'root'
  options[:keys] ||= File.expand_path("#{ENV['HOME']}/.ssh/my_staging_key" ,__FILE__)

  set :host,        options[:host_name] || host
  set :ssh_options, options

  # Disable sudo
  # set :disable_sudo, true

  RSpec.configure do |config|
    config.color = true
    config.tty = true
  end

  # Set environment variables
  set :env, :LANG => 'C', :LC_MESSAGES => 'C'
end

3.2. テスト実行コマンド

3.2.1. role対象サーバの確認

roleはRakefileの構成により環境変数(SPEC_ENV)により、本番環境(SPEC_ENV=production)と検証環境(SPEC_ENV=staging)を分けることが出来ように実装しています。また、さらにサーバ種別毎(spec:web:all)やサーバ単体(spec:web:web03.domain.com)、全て(spec:all)とそれぞれ実行できるようにしています。

環境毎のyamlが正常に設定されているか確認します。環境変数(SPEC_ENV)を指定しない場合は、defaultでhosts_staging.yamlを読み込みます。

# bundle exec rake -vT
rake spec:all                    # all target for staging
rake spec:app:all                # all target of app for common,group/system,user/system,user/ap,ke...
rake spec:app:ap01.sdomain.com   # Run serverspec tests to app: ap01.sdomain.com for common,group/s...
rake spec:db:all                 # all target of db for common,group/system,user/system,user/db,ker...
rake spec:db:db01.sdomain.com    # Run serverspec tests to db: db01.sdomain.com for common,group/sy...
rake spec:web:all                # all target of web for common,group/system,user/system,user/web,k...
rake spec:web:web01.sdomain.com  # Run serverspec tests to web: web01.sdomain.com for common,group/...
rake spec:web:web02.sdomain.com  # Run serverspec tests to web: web02.sdomain.com for common,group/...

SPEC_ENV=productionを指定し本番環境でのyamlが正常に設定されているか確認。

# SPEC_ENV=production bundle exec rake -vT
rake spec:all                   # all target for production
rake spec:app:all               # all target of app for common,group/system,user/system,user/ap,kernel/system,kernel/ap,firewall/ap,rpm/ap,service/ap,cro...
rake spec:app:ap01.domain.com   # Run serverspec tests to app: ap01.domain.com for common,group/system,user/system,user/ap,kernel/system,kernel/ap,firewa...
rake spec:app:ap02.domain.com   # Run serverspec tests to app: ap02.domain.com for common,group/system,user/system,user/ap,kernel/system,kernel/ap,firewa...
rake spec:db:all                # all target of db for common,group/system,user/system,user/db,kernel/system,kernel/db,firewall/db,rpm/db,service/db,cron...
rake spec:db:db01.domain.com    # Run serverspec tests to db: db01.domain.com for common,group/system,user/system,user/db,kernel/system,kernel/db,firewal...
rake spec:db:db02.domain.com    # Run serverspec tests to db: db02.domain.com for common,group/system,user/system,user/db,kernel/system,kernel/db,firewal...
rake spec:web:all               # all target of web for common,group/system,user/system,user/web,kernel/system,kernel/web,firewall/web,rpm/web,service/we...
rake spec:web:web01.domain.com  # Run serverspec tests to web: web01.domain.com for common,group/system,user/system,user/web,kernel/system,kernel/web,fir...
rake spec:web:web02.domain.com  # Run serverspec tests to web: web02.domain.com for common,group/system,user/system,user/web,kernel/system,kernel/web,fir...
rake spec:web:web03.domain.com  # Run serverspec tests to web: web03.domain.com for common,group/system,user/system,user/web,kernel/system,kernel/web,fir...
rake spec:web:web04.domain.com  # Run serverspec tests to web: web04.domain.com for common,group/system,user/system,user/web,kernel/system,kernel/web,fir...
[root@sakamoto_test ~]#

3.2.2. role対象サーバのテスト実行

# SPEC_ENV=production bundle exec rake spec:all  #本番環境の全てのサーバ
# SPEC_ENV=production bundle exec rake spec:db:all  #本番環境のdbサーバ全て
# SPEC_ENV=production bundle exec rake spec:web:web01.domain.com  #本番環境のweb01.domain.comサーバのみ

3.2.3 実行結果確認

# cat audit/json/#{host}.json | jq '.'

4. テストスクリプト記述方法

テストスクリプトのフォーマットは以下のようになっています。

各リソースタイプの詳細は、公式HP リソースタイプを参照。

describe <リソースタイプ>(<テスト対象>) do
  <テスト条件>
  :
  :
end

インストールパッケージ

describe package('httpd') do  ←「httpd」パッケージに関するテスト
  it { should be_installed.with_version('2.4.6-80') }  ←パッケージがインストールされているか?
end

サービス設定及び状態

describe service('httpd') do  ←「httpd」サービスに関するテスト
  it { should be_enabled   }  ←サービスが有効になっているか?
  it { should be_running   }  ←サービスが実行されているか?
end

ポート状態

describe port(80) do  ←80番ポートに関するテスト
  it { should be_listening }  ←ポートが待ち受け状態になっているか?
end

ファイル

describe file('/etc/httpd/conf/httpd.conf') do ←「/etc/httpd/conf/httpd.conf」ファイルに関するテスト
  it { should be_file }  ←ファイルが存在するか?
  its(:content) { should match /ServerName localhost/ }  ←ファイル内に「/ServerName localhost/」にマッチするテキストが存在するか?
    it { should be_owned_by('root') }
    it { should be_grouped_into('root') }
    it { should be_mode 755}
end

ディレクトリ

describe file("/var/path/directory/") do
  it { should be_directory }           # directoryかどうか
  it { should be_owned_by 'root'}      # オーナーがrootか
  it { should be_grouped_into 'root'}  # グループがrootか

  # 中でif文を書くことも出来ます
  if dir1 == 'conf'
    it { should be_mode 700}
  else
    it { should be_mode 755}
  end
end

cron エントリー

describe cron do
  it { should have_entry '* * * * * /usr/local/bin/foo' }
end

cron エントリー(特定ユーザ)

describe cron do
  it { should have_entry('* * * * * /usr/local/bin/foo').with_user('foo') }
end

group

describe group('foo_group') do
  it { should exist }
  it { should have_gid 100 }
end

User

describe user('foo') do
  it { should exist }
  it { should belong_to_group 'foo_group' }
  it { should have_uid 100 }
  it { should have_home_directory '/home/foo' }
  it { should have_login_shell '/bin/bash' }
  it { should have_authorized_key 'ssh-rsa <SSH公開鍵> foo@bar.local' }
end

ネットワークインターフェース設定

describe interface('eth0') do
  its(:speed) { should eq 1000 }
  it { should have_ipv4_address("192.168.10.10") }
  it { should have_ipv4_address("192.168.10.10/24") }
end

ネットワーク疎通

describe host('web01.domain.com') do
  # ping
  it { should be_reachable }
  # tcp port 22
  it { should be_reachable.with( :port => 22 ) }
  # set protocol explicitly
  it { should be_reachable.with( :port => 22, :proto => 'tcp' ) }
  # udp port 53
  it { should be_reachable.with( :port => 53, :proto => 'udp' ) }
  # timeout setting (default is 5 seconds)
  it { should be_reachable.with( :port => 22, :proto => 'tcp', :timeout => 1 ) }
end

デフォルトゲートウェイ

describe default_gateway do
  its(:ipaddress) { should eq '192.168.1.1' }
  its(:interface) { should eq 'eth0'          }
end

静的ルーティング

describe routing_table do
  it do
    should have_entry(
      :destination => '192.168.100.0/24',
      :interface   => 'eth1',
      :gateway     => '192.168.10.1',
    )
  end
end

firewalld

describe firewalld do
    its(:default_zone) { should contain 'public' }
    it { should have_port('161/udp') }
    it { should have_service('ssh') }
    it { should have_source('192.160.0.100/32') }
    it { should have_interface('eth0') }
  end

カーネルパラメータ

describe 'Linux kernel parameters' do
  context linux_kernel_parameter('net.ipv4.tcp_syncookies') do 
    its(:value) { should eq 1 }
  end

  context linux_kernel_parameter('kernel.shmall') do
    its(:value) { should be >= 4294967296 }
  end

  context linux_kernel_parameter('kernel.shmmax') do
    its(:value) { should be <= 68719476736 }
  end

  context linux_kernel_parameter('kernel.osrelease') do
    its(:value) { should eq '2.6.32-131.0.15.el6.x86_64' }
  end

  context linux_kernel_parameter('net.ipv4.tcp_wmem') do
    its(:value) { should match /4096\t16384\t4194304/ }
  end
end

確認コマンド(LDAP search)

ldapsearch_command = "ldapsearch -x -h 127.0.0.1 -b "認証に必要な情報" -w #{password}"

describe command("#{ldapsearch_command} \"uid=idname\"  |grep 'ftpUID:'|awk '{print $抜き取る場所}'") do
  its(:exit_status) {should match eq 0} 
  its(:stdout) {should match 検証する文字列}
end

確認コマンド(Webサイトへのアクセス確認)

hostname=host_inventory['hostname']
# httpd port open check
describe command("curl http://#{hostname}/wp-admin/install.php") do
  its(:stdout) { should contain('WordPress') }
end

最後に

こんなにも簡単にテストできるなんて、驚きの一言です。それも早い。
繰り返しテストを実施しなければならない状況なら尚更ですね。

テストスクリプトもバージョン管理できるし、工夫すれば実行結果をCSV化することも可能なのでヘッダーを付けExcelすればテスト結果報告書の作成もあっと言う間にできちゃいます。

もちろん、全てをserverspecで自動化することは不可能でしょうが、部分的に自動化することができるだけでも工数削減はいとも簡単に実現できることは確実でしょう。

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

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

概要

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

Sample.java
import java.io.*;

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

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

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

原因と対処方法

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

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

# locale -a
C
POSIX
en_US.utf8

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

LANG=ja_JP.UTF-8
export LANG

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

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

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

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

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

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

結論

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

[メモ]複数ログファイルを結合&ソートする

メモです

複数サーバーで出力されているログをまとめてみたかったので調べました。
WindowsでもGitbashで動いたのでとても便利。

ls XXX.* | xargs cat | sort > concated_sorted.log
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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

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

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

Docker

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

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

Container と Windows

1. 背景

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

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

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

2. 沿革

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3. 用語の整理

:book: Linux Container

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

:book: Windows Container

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

:book: Windows Server Container

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

Windows process container とも呼ばれる。

:book: Hyper-V Container

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

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

:book: LCOW ( Linux Containers on Windows )

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

Docker Desktop エコシステム復習

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

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

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

Windows

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

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

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

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

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

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

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

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

● Docker.Watchguard
全くの謎。

Linuxkit

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

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

接続

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

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

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

Build

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

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

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

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

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

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

$ ctr namespace ls
# NAME              LABELS
# services.linuxkit

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

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

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

dockerd

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

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

Persistence Data

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

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

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

Volume Sharing

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

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

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

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

docker-Page-3 (2).png

Network

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

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

Host Namespace

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

Container Namespace

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

Interface: eth0

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

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

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

Interface: hvint0

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

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

Network: docker0, vethXXXX@ifXXX

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

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

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

docker-Page-4 (2).png

Network: cni0, vethXXXX@eth0

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

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

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

docker-Page-5.png

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

Tunnel: tunl0@NONE, ip6tnl0@NONE

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

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

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

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

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

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

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

Host-Guest 間 socket 通信

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

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

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

Hyper-V Socket

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

● Socket Address Family

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

● Guest Communication Service

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

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

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

● Service GUID 命名規則

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

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

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

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

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

docker-Page-6.png

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

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

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

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

vsudd & com.docker.proxy.exe

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

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

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

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

VPNKit

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

以下、主要なサービス。

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

● vpnkit-tap-vsockd

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

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

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

● vpnkit-forwarder

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

前準備

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

docker-Page-10.png

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

Port Forwarding

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

Container を立ち上げる。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

● Windows Named Pipe

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

● DNS

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

hosts
...

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

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

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

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

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


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

● Diagnosis

Log : Docker Desktop

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

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

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

● HyperV
Hyper-V の操作ログ。

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

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

Log : LinuxKit

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

まとめ

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

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

次回に続く。

おまけ

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

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

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

Kubernetes が起動しない

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

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

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

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

参考

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

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

GriveでGoogleドライブをLinuxマシンと同期する

Googleドライブは便利ですが、自動でファイルエクスプローラーからアクセスできる機能は、WindowsやAndroid等でしか提供されておらず、Linuxマシンをデスクトップとして使っている場合不便です。

ここでは、Googleドライブのディレクトリの内容を、ローカルの特定ディレクトリの内容と同期して、ファイルをいい感じに移動させたりしたいと思います。

Grive2のインストール

griveは更新が2年ほど前で終わっていますが、有志がフォークしてgrive2を作っているのでそれを使います。

https://github.com/vitalif/grive2 のREADME.mdやwikiに書いてあるとおり、CMakeでビルドしてインストールするのですが、自分はArchlinuxのAURがあったのでそれを使いました。

ディレクトリの設定

cd $HOME
mkdir google-drive
cd google-drive
grive -a

grive -aを実行すると、URLが表示されるので、コピーしてブラウザで開きます。
開くと、Googleの認証画面でGoogle Driveへのアクセス権を尋ねられるので、連携or許可をクリックします。

そうすると、認証キーが表示されるのでターミナルに貼り付ければ完了です。

あとは自動でGoogleドライブ上の全ファイル・ディレクトリがダウンロードされます。

自動同期

systemctl --user enable grive-timer@$(systemd-escape google-drive).timer
systemctl --user start grive-timer@$(systemd-escape google-drive).timer
systemctl --user enable grive-changes@$(systemd-escape google-drive).service
systemctl --user start grive-changes@$(systemd-escape google-drive).service

上の例では、google-driveという名前のホームディレクトリ直下のディレクトリを同期しています。
grive-timer@.timerで5分間隔の自動ダウンロード、grive-changes@.serviceでinotifyファイル検知を使って自動アップロードをしています。

これで、Google Driveでファイルの作成/削除やローカルでファイルの作成/削除をすると、同期されるようになりました。

※AURにはsystemdのファイルがなかったので、ソースから.inを直接ダウンロードして手直しして入れました。

まとめ

内容が、README.mdを日本語訳しただけなので、ほどんど「いかがでしたかブログ」並に薄くなってしまいましたが、役に立てば嬉しいです。

今回のgrive2は、必要になったらファイルの実態をダウンロードする方式(google drive ocamlfuse mount)とは違い、単純にダウンロードして差分同期を取る方式でした。

自分はネットワーク帯域もストレージ帯域も余裕が有り、こちらのほうが便利で使いやすかったのですが、これは人によって異なると思うので、両方試して自分が使い勝手の良い方を使ってみると良いと思います。

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