Docker Desktop for MACのDockerホストの仮想マシン(HyperkitのVM)に入りたい

Dockerでローカルストレージの名前付きボリュームを作成した際に、マウントポイントの実体を確認したかった。

ただそれだけ。

環境

ボリュームを作成する

まず、適当なローカルストレージのボリュームを作成する。

$ docker volume create sample
sample

作成したボリュームをinspectしてマウントポイントを確認。

$ docker volume inspect sample
[
    {
        "CreatedAt": "2021-10-23T15:57:48Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/sample/_data",
        "Name": "sample",
        "Options": {},
        "Scope": "local"
    }
]

ボリュームの実体は/var/lib/docker/volumes/sample/_dataにある事が分かった。

ただ、このパスはDockerホストが稼働している仮想マシン上に存在する為、ターミナルで普通に探しても見つからない。

$ ls /var/lib/docker/volumes/sample/_data
ls: /var/lib/docker/volumes/sample/_data: No such file or directory

Docker Desktop for MACは、HyperkitというmacOSのHypervisor.frameworkのみをサポートしているアプリケーションにHypervisorの機能を組み込むツールを利用しており、DockerホストはこのHyperkitの仮想マシンLinuxkitを使って構成したコンテナランタイム環境(containerd&runc)で動いている。

その為、上述のマウントポイントの実体を確認するには、Dockerホストが稼働している仮想マシンに入る必要がある。

で、Dockerホストに入る方法を調べてみると、以下の様なscreenコマンドを使ってDockerホストのttyにアクセスする方法が出てくるのだが、

$ screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty

自分の環境ではパーミッションエラーとなり、rootでもパーミッションを変更出来なかった為、この方法では無理だった。

その為、別の方法を探していたところ、nsenterを使って特権モードのコンテナを起動することで、Dockerホストに入ることが出来ると分かった。

nsenterを利用すると、docker execとは異なり、cgroupに縛られることなく、指定のコンテナのプロセス空間に対して、外から新しいプロセスを生成出来るようになる。

nsenterコマンドはalpineにインストールされているので、以下の様なコマンドを実行することで、Dockerホストに入ることが可能となる。

$ docker run -it --rm --privileged --pid=host alpine:edge nsenter -t 1 -m -u -n -i sh
/ # 

--pidにhostを指定してコンテナ内からDockerホストのプロセスを見えるようにし、--privilegedでDockerホストリソースへのアクセスを許可することで、nsenterを使ってDockerホスト側のプロセス空間にshプロセスを生成している訳である。

これで、Dockerホストに入ってボリュームのマウントポイントの実体を確認することが出来た。

$ docker run -it --rm --privileged --pid=host alpine:edge nsenter -t 1 -m -u -n -i sh
/ # ls -al /var/lib/docker/volumes/sample/_data
total 8
drwxr-xr-x    2 root     root          4096 Oct 23 15:57 .
drwx-----x    3 root     root          4096 Oct 23 15:57 ..
/ #

ちなみに、nsenterはalpineのみならず、debianubuntucentos等、大概のコンテナOSイメージにはインストールされているので、それらのイメージを使っても同様にDockerホストへのアクセスは可能。

また、調べていたらnsenter1という、nsenterのコマンド入力を省略するイメージも存在していたので、こっちを使っても良いかも。

$ docker run -it --rm --privileged --pid=host justincormack/nsenter1
/ #

Ansible Night in Tokyo 2018.04に参加してきた

RedHat社のセミナールームで開催されたAnsible Night in Tokyo 2018.04に参加してきました。

ansible-users.connpass.com

Ansibleユーザー会主催のイベントに参加するのは第1回もくもく会以来。

80名の参加枠に対して240名くらいの応募という人気っぷりで、抽選の結果、見事に外れた上、補欠順位も200番以降となってしまい、これは絶望的だなぁと一時は落胆したものの、ブログ枠が空いていたので滑り込んだ次第です。

そんな訳で、各講演を拝聴しての雑感を書き残しておきます。

Cisco with Ansible

シスコシステムズ加藤久慶さん、横石 雄大さん、畝高 孝雄さんの講演。お三方それぞれのテーマで発表。

NexusでAnsibleやってみた

まず、Ciscoについて。日本ではルーターやスイッチのイメージ強いけど、それらは一部のサービスで、ネットワークに関わる様々な製品を提供しているとのこと。

この発表ではCiscoのスイッチNexusシリーズ4台を使ったネットワークを、Ansibleで自動的に構築(スイッチ間でのP2Pセグメント設定、Loopbackインターフェースの作成、OSPFを有効にしての経路伝播等)するところをサクッとデモされていました。

NexusシリーズのOSであるNX-OS用のAnsibleモジュールは現在72種類あり、大概のことは自動化できるそうです。

なお、CiscoにはDevNetという開発者向けのサービスがあり、そちらに各種サービスのSandboxが提供されているので、実機の購入という高いハードルを越えずとも、気軽に検証することが可能とのこと(DevNetは様々なSSOに対応しており、GithubFacebook等のアカウントでログインできます)。

また、Learning Labsというコンテンツもあり、Cisco製品におけるAnsibleの活用方法や、Ansible自体の使い方等について学習することができる(Ansible関連以外のものも)ので、是非、有効に活用してもらいたいとのことでした。

Contiv + k8s + Ansible

Contivについての紹介がメインな発表。メモはとったものの、発表内容の要点はCiscoのブログにまとまっていたので、そちらの方が参考になります。

実務でコンテナ使えていないこともあって、正直、Contivについては全く知らなかったのだけれど、深掘りしたい内容でした。

Cisco ACIとAnsibleのステキな関係

www.slideshare.net

Ansibleの制御対象は増えるほど管理が大変。サーバーという点に対し、面であるネットワークの構成管理は勝手が違う。

という問題に対し、ACI(+APIC)の紹介をメインに、Ansibleとの連携でネットワーク構成管理の煩雑さがどう解消されるのかといった発表でした(多分)。ACI(+APIC)については自分のメモよりCiscoの製品紹介を見た方が参考になります。

ACIのAnsibleモジュールは、充実したドキュメントが公式に用意されているので、検証する際はそちらを参照すると良いとのこと。

なお、ACIにはacitoolkitというPython製のツール群があり、その内のACI Diagram generatorを利用すると、APICAPIアクセスしてACIのダイアグラムをグラフィカルに作成することができるのだとか。

Ansible2.5のアップデートとネットワーク自動化

RedHat社のAnsibleビジネスユニット プリンシパル・プロダクト・マネージャー、Sean Cavanaughさんの講演。

英語で通訳無しであった為、残念ながらあまり理解できませんでしたが、概ね以下のようなお話をされていたかと思います。

Ansibleを追いかける際の情報源を知れたのは収穫でした。

LT1:ANSIBLE ROLEの継続的自動UPDATE

chrojuさんの発表。

speakerdeck.com

Ansible Roleをソフトウェアパッケージに見立て、yarnやbundle、Gemfile.lock等を参考に工夫を凝らしてCIを回すというお話。

個人的にAnsibleはまだまだミニマムな使い方しかしていないけれど、ゆくゆくはぶつかりそうな問題なので参考になりました。

LT2:Junos モジュールのコネクションタイプの使い分け

akira6592さんの発表。

www.slideshare.net

英語の講演中に理解しきれなかった2.5から追加されたコネクションタイプについて、こちらのLTで補完されました。

LT3:Ansibleを使用したWindows管理

curry9999さんの発表。

speakerdeck.com

なかなか出来上がった状態での発表となったcurry9999さん。笑いあり介護ありで面白かったですw

AnsibleのWindowsモジュールは1.7からじわじわ増え、2.5では81種類となり、自動化できる範囲も広がっているので、積極的に活用して無駄な運用タスクを減らしていこうというお話。

curry9999さんはライフワーク(?)でAnsibleモジュールの歴史をまとめられているので、定期的にチェックさせてもらっています。

Ansible 2.6 対応 モジュールの歴史 フルアーマー版awsbloglink.wordpress.com

LT4:Ansible とStackStorm でつくるChatOps 環境

katsuhisaさんの発表。

speakerdeck.com

StackStormを活用したChatOpsの自動化について。

運用においてChatが有効活用できていない環境にいるので、ここまで自動化できてるのは個人的には未来感ありました。

IFTTTって知らなかった。トライしたい。

LT5:Windowsのcp932に苦闘している

Hidetoshi Hirokawaさんの発表。

www.slideshare.net

AnsibleでWindowsの自動化を進めていくとぶち当たる、文字コードという特有の問題に対するお話。

Windows文字コード問題は本当に苦労しますね…。

終わりに

知見の乏しいネットワーク領域に関する講演がメインであったので、その場では理解しきれないことも多かったのですが、日頃、サーバー用途でしかAnsibleを使っていない自分にとっては、全体的に新鮮な内容でありました。改めてAnsibleの適用範囲って広い。

なお、イベント終了後は恒例となっているAnsible飯に参加させて頂き、Ansibleに関する話題は当然として、バーベキューとか肉の妖精とかIoTプラレールとか、参加者の皆さんの興味深い趣味の話にも花が咲き、今回も有意義な時間を過ごさせてもらいました。

イベント主催者、スタッフの皆さん、RedHat社に感謝致します。有難うございました。

※ 会場が恵比寿だからか、イベント開始前にRedHat社の望月社長からヱビスビールが振る舞われました。ありがたやありがたや。

f:id:heroween:20180426184640j:plain

※ ブログ書くということでAnsibleノートも頂きました。ありがたやありがたや。

f:id:heroween:20180426185832j:plain

PackerでAMIを作ろうとしたら吐かれたsudoエラーについて

久々にPackerを使ってみて遭遇したエラーについてメモしておく。

環境

動作環境はMac Book AirOSX 10.8 Mountain Lion。Packerはv0.8.6を使用。
AMIにはAmazon Linux AMI 2015.09.1 (HVM), SSD Volume Type - ami-383c1956を選択。

Packerテンプレート

単純にAmazon LinuxのEC2インスタンスsudo yum update -yしたAMIを作るだけのものである。

{
  "variables": {
    "aws_access_key": "",
    "aws_secret_key": ""
  },

  "builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "ap-northeast-1",
    "source_ami": "ami-383c1956",
    "instance_type": "t2.micro",
    "ami_name": "wps-web-ami {{timestamp}}",
    "ssh_username": "ec2-user",
    "ssh_timeout": "5m",
    "tags": {
      "Name": "WordPress Server"
    }
  }],

  "provisioners": [{
    "type": "shell",
    "inline": [
      "sudo yum update -y"
    ]
  }]
}

エラーメッセージ

上記のテンプレートでビルドしたら以下の様なメッセージを吐いてコケた。

$ packer build ami.json 
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name...
==> amazon-ebs: Inspecting the source AMI...
==> amazon-ebs: Creating temporary keypair: packer 5655d9e2-b5e7-8a86-9f64-33936ce5add0
==> amazon-ebs: Creating temporary security group for this instance...
==> amazon-ebs: Authorizing access to port 22 the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
    amazon-ebs: Instance ID: i-ed34aa48
==> amazon-ebs: Waiting for instance (i-ed34aa48) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with shell script: /var/folders/_p/5nbnyk0n5x599chrtk9vswqc0000gn/T/packer-shell560434101
    amazon-ebs: sudo: sorry, you must have a tty to run sudo
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: No AMIs to cleanup
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' errored: Script exited with non-zero exit status: 1

==> Some builds didn't complete successfully and had errors:
--> amazon-ebs: Script exited with non-zero exit status: 1

==> Builds finished but no artifacts were created.

原因と解決方法

エラーメッセージの赤く着色した部分が問題なのだが、読んだ通りで仮想端末を使わなければsudoできませんよと言われている。
一年くらい前にPackerで同じ様にAMIを作っていた時には、この設定内容で問題無かったような気がしたのだが、どういうことだろうと思い調べてみたところ、PackerのChangeLogにその答えが書いてあるのを見つけた。

core: SSH connection will no longer request a PTY by default. This can be enabled per builder.

Packerは2015年6月23日にv0.8.0へとアップデートしたのだが、その際、標準ではSSH接続にPTYを要求しなくなったそうだ。要するに、リモートマシンに仮想端末でログインせず、直接コマンドを投げているので、sudoersに外部からのコマンド実行を遮断されている訳である。
この問題を解決するのは簡単で、SSH接続にPTYを要求するよう設定してやれば良い。v0.8.2から追加されたCommunicatorのオプションであるssh_ptytrueを設定し、上記テンプレートのBuilder部分に追記してやると、SSH接続に仮想端末を使うようになるので、Provisionerのsudoコマンドが実行されるようになる。
なお、他の解決方法として、AMI Builderのuser_dataを利用し、sudoersの設定内容を書き換えてしまうということも可能ではあるのだが、当然、セキュリティ的に問題があるのでおすすめはしない。素直にssh_ptyを使った方が手間が無いし安全だろう。

ChefのパッケージをWeb API(Omnitruck API)で取得する

Chefのパッケージ取得方法について、ちょっとしたメモ。
Chef ClientやChef Serverのパッケージは、ダウンロードページから以外にも、Web APIを利用した取得方法が存在する。

Omnitruck API

Chef社はOmnitruck APIというWeb APIを公開しており、任意のURLをリクエストすることにより、リリースされているあらゆるバージョンのChef ClientやChef Serverのパッケージを取得することができる。
また、通常のダウンロードページには存在しない、プレリリース版やナイトリー版の取得にも対応していたりする。
Chef ServerのCommunity Cookbookなんかでは、OhaiとOmnitruck APIを利用して、環境に合わせた臨機応変なパッケージの取得を実現している。

Chef Clientの取得方法

以下のURLをリクエストすることで、パッケージを取得できる。

http://www.getchef.com/chef/download?p=$PLATFORM&pv=$PLATFORM_VERSION&m=$MACHINE_ARCH&v=latest&prerelease=false

また、上記URLのdawnloadをmetadataに変更すると、パッケージに付随したメタデータを取得することができる。

http://www.getchef.com/chef/metadata?p=$PLATFORM&pv=$PLATFORM_VERSION&m=$MACHINE_ARCH&v=latest&prerelease=false

メタデータは、パッケージのダウンロードURL、MD5とSHA256のチェックサムが記されたタブ区切りのテキストデータである。

url http://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chef-11.16.0-1.el6.x86_64.rpm
md5 5c8cfdbab2684148e2bb859b736b6827
sha256 e4d0236cc495d080f8e7a01704e2ef554e12088687e4fe946ee8027b79871bbb 

クエリパラメーターについては、

  • pはプラットフォーム。debian、el(rhel系)、freebsdmac_os_x、solaris2、sles(SUSE Enterprise Linux Server)、suseubuntuwindowsといった値を指定する。
  • pvはプラットフォームバージョン。プラットフォームに応じて指定できる値は決められており、elであれば5や6、ubuntuであれば10.4や12.10等、mac_os_xであれば10.6や10.7等といった具合。
  • mはマシンアーキテクチャi386i686x86_64、amd64といった値を指定するが、これもプラットフォームに依存する。
  • vはChef Clientのバージョン。デフォルトはlatestで省略可能。特定のバージョンが欲しい場合は、Semantic Versioningに則った形式で、x.y.z(x=MAJOR、y=MINOR、z=PATCH)といった具合に指定する。
  • prereleaseはプレリリース版を取得するためのフラグ。デフォルトはfalseで省略可能。

といった感じ。
これらクエリパラメーターでサポートされている値のより詳細な情報は、公式のドキュメントの方を参照してほしい。

Chef Serverの取得方法

Chef ClientのダウンロードURL末尾に-serverを付与するとChef ServerのダウンロードURLとなる。

http://www.getchef.com/chef/download-server?p=$PLATFORM&pv=$PLATFORM_VERSION&m=$MACHINE_ARCH&v=latest&prerelease=false&nightlies=false

メタデータの取得URLも同様。

http://www.getchef.com/chef/metadata-server?p=$PLATFORM&pv=$PLATFORM_VERSION&m=$MACHINE_ARCH&v=latest&prerelease=false&nightlies=false

クエリパラメーターも基本的にChef Clientと同じ構成であるが、フラグが一つ追加されている。

  • nightliesはナイトリー版を取得するためのフラグ。デフォルトはfalseで省略可能。ナイトリー版はあくまで検証用として用意されているので、本番環境では決して使わないこと。

当然だが各クエリパラメーターで指定できる値はChef Clientとは異なるので、サポートされている値の詳細な情報は、公式のドキュメントの方を参照してほしい。
公式ドキュメントには、他にも取得例が記載されているので、自前でChef ClientやChef Serverのインストール自動化プログラムを作る際には参考になるだろう。

ドメインについて

最後に、Chef社の旧ドメインについても触れておく。
現在のChef社は、社名変更に伴ってgetchef.comドメインを使っているが、変更前のOpscode社時代ではopscode.comドメインが使われていた。
このドメインはまだ生きており、opscode.comにアクセスすると、getchef.comへとリダイレクトされることが分かる。
で、Omnitruck APIについてだが、こちらも以前のURLであるwww.opscode.comでリクエストしても、www.getchef.comでリクエストした場合と同様に、正常にパッケージを取得することができる。
ちなみに、冒頭で例に挙げたChef ServerのCommunity Cookbookなんかは、このエントリを書いた時点では、旧ドメインのままだったりする。
とはいえ、いずれはサポートの切れるかもしれないドメインではあるので、これからOmnitruck APIを利用する場合に、わざわざ旧ドメインAPIを使用するのは止めておいた方が良いだろう。

CentOS6.5でランダムSalt付きSHA-512のシャドウパスワードを生成する

CentOS6.5でランダムSalt付きSHA-512のシャドウパスワードを生成する方法を調べたのでメモしとく。

最初、OpenSSLでできるかなと思いついたが、openssl passwdMD5はいけるけど、SHAには対応しておらず駄目だった。

$ openssl passwd --help
Usage: passwd [options] [passwords]
where options are
-crypt             standard Unix password algorithm (default)
-1                 MD5-based password algorithm
-apr1              MD5-based password algorithm, Apache variant
-salt string       use provided salt
-in file           read passwords from file
-stdin             read passwords from stdin
-noverify          never verify when reading password from terminal
-quiet             no warnings
-table             format output as table
-reverse           switch table columns

他にその手のツールはないか調べたところ、要件にピッタリ合致するgrub-cryptコマンドというのを見つけた。

$ grub-crypt --help
Usage: grub-crypt [OPTION]...
Encrypt a password.

  -h, --help              Print this message and exit
  -v, --version           Print the version information and exit
  --md5                   Use MD5 to encrypt the password
  --sha-256               Use SHA-256 to encrypt the password
  --sha-512               Use SHA-512 to encrypt the password (default)

Report bugs to <bug-grub@gnu.org>.
EOF

このコマンドを使うと、以下のように対話式でランダムSalt付きのSHA-512シャドウパスワードが生成できる。

$ grub-crypt         ←デフォルトがSHA-512なのでオプション必要無し
Password:            ←パスワード入力(今回はsampleという文字列)
Retype password:     ←もう一度パスワード入力
$6$5TIs9aRT3V4IRwJz$guTjCJzy1k.t8Gy8Y0TQiY25S7y6ptVzxeS0/o1JJG52Xv32lVIGCqOh11QCSeTqSlR2CVI380kTHYEY6WWOq/

うんうん、ちゃんとシャドウパスワードの形式で出力されてるね。
とてもシンプルで良いコマンドです。

Cygwinをアンインストールする

Cygwinをアンインストールしようと思ったが、Cygwinにはアンインストールを自動で行う手段が無く、手作業で行う必要があったので、その手順をメモしておく。

アンインストール手順

手順は公式サイトのFAQに記載されている。

http://cygwin.com/faq/faq.html#faq.setup.uninstall-all

意訳すると、

  1. Cygwin内でサービスを起動している場合は以下の手順で全て削除しましょう。一般的にインストールされていそうなサービスはsshd、cron、cygserver、inetd、apachepostgresqlとかです。

    1. cygrunsrv -Lで登録しているサービスを確認します。cygrunsrvが無い場合はこの過程を飛ばしましょう。

    2. 起動しているサービスがあればcygrunsrv --stop service_nameで停止します。inetdを使ってスタンドアローンなサービスを起動している場合はcygrunsrv -Lで表示されませんが、cygrunsrv --stop inetdを実行すればそれらもまとめて停止します。

    3. 最後にcygrunsrv --remove service_nameで各サービスを削除しましょう。

  2. X11サーバーが起動していれば停止し、バックグラウンドで動いているCygwinプログラムも止めます。コマンドプロンプトを終了し、Cygwinプロセスが無い事を確認しましょう。もし、再インストールする時の為にマウントポイントを保存しておきたければ、mount -mでfstabに書き込んでおくと良いでしょう。

  3. cyglsa.dllをインストールしており、/usr/bin/cyglsa-configを実行していた場合、LSA authentication packageの使用を停止する必要があります。その為、レジストリの/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Lsa/Authentication Packagesの値を、元のmsv1_0という値に戻してWindowsを再起動しましょう。

  4. Cygwinのルートフォルダとサブフォルダを全て削除します。オブジェクトが使用されているといったエラーが出ましたら、全てのサービスが停止されて、全てのCygwinプログラムが終了しているかを確認しましょう。パーミッションエラーが出ましたら、エラーの原因となっているファイルやフォルダのパーミッションを変更しましょう。例えば、システムサービスによって使用されているファイルはSYSTEMアカウントが保持し、一般ユーザーからは書き込みができなくなります。

    手っ取り早く削除するには、自分に全てのファイルとフォルダの所有権限を付与しましょう。Windows Explorerを開き、Cygwinのルートフォルダで右クリックし、プロパティからセキュリティタブを選択します。もし、Windows XP Home Editionのシンプルファイル共有を使っているならば、これはセーフモードにしてからやる必要があります。それから、自分のアカウントを選択し、フルコントロールできるように変更すればOKです。まとめて削除できるようになるでしょう。

  5. デスクトップとスタートメニューのショートカット、setup-x86{_64}.exeがインストール時に使ったダウンロードフォルダを削除します。しかし、再インストールする場合であれば、setup-x86{_64}.exeは既にあるダウンロードフォルダをキャッシュとして再利用できるので、残しておくと再ダウンロードせずに済むでしょう。

  6. システムパスにCygwinを通していたら、同じ場所に再インストールする予定が無い限りは削除しましょう。CYGWIN環境変数も同様です。

  7. 最後に、徹底的にCygwinの痕跡を消したければ、レジストリのHKEY_LOCAL_MACHINEとHKEY_CURRENT_USER配下にある、Software/Cygwinを削除しましょう。しかしながら、ここまでの手順に従っていれば、既に削除すべきものは削除できています。通常、インストールフォルダは全てのレジストリに保持されています。

と、まあこんな感じか。

で、この通りにやって特に問題無くアンインストールできました。

MSYSのrxvtやCygwinでVagrantを実行した際に出力されるNIO4RのDLLエラーについて

Vagrantに内包されているMSYSのrxvtやCygwinで、Vagrantのコマンドを叩いた時に検出されたエラーについてメモしとく。
あらかじめ書いておくが、問題の回避はできたものの、根本的な原因の究明にまでは到っていない。

環境

VagrantWindows版のパッケージを使っていて、vagrant-berkshelfはコマンドプロンプトからインストールしている。

事象

コマンドプロンプトからVagrantのコマンドを実行すると問題無く動いていたのだが、Vagrantに内包されているMSYSのrxvtや、Cygwinから実行すると以下のエラーが出力されて止まってしまった。

$ vagrant status
Vagrant failed to initialize at a very early stage:

The plugins failed to load properly. The error message given is
shown below.

1114: ダイナミック リンク ライブラリ (DLL) 初期化ルーチンの実行に失敗しました。   - C:/Users/heroween/.vagrant.d/gems/gems/nio4r-1.0.0/lib/nio4r_ext.so

なんでかなーと原因を調べてみた。

原因を探る

まず、問題を起こしているNIO4Rについてだが、これはBerkshelfが依存しているGemであり、Vagrantプラグインであるvagrant-berkshelfをインストールすると一緒に入ってくる。
このNIO4Rはクロスプラットフォームな作りとなっており、実行環境のRubyの処理系に応じて実装が変わる。

  • MRI/YARV、Rubiniusの場合はlibevをベースとしてC言語で実装された拡張ライブラリが使われる。
  • JRubyの場合はJava NIOをベースとしたJavaで実装された拡張ライブラリが使われる。
  • 上記以外であればPure Ruby実装のライブラリが使われる。

と、READMEではこのように説明されている。
Pure Rubyだと処理速度が遅いので、CやJava実装の拡張ライブラリも使えるようになっているのだろう。

で、今回はVagrantに内包されている、WindowsRuby(MRI)で実行したら問題が起きた。
上記の説明の通りであれば、MRIなのでC実装の拡張ライブラリが使われて、その際にDLLエラーが発生した…ものと思われるのだが、腑に落ちない点がある。
MSYSのrxvtやCygwinで出るエラーが、コマンドプロンプトでは出ずに、普通に動いているという点だ。

よく分からないので、実装の変わる条件がどうなっているのか、インストールされたNIO4Rのコード(nio.rb)を確認してみた。

if ENV["NIO4R_PURE"] || (ENV["OS"] =~ /Windows/i && !defined?(JRUBY_VERSION))
  require 'nio/monitor'
  require 'nio/selector'
  NIO::ENGINE = 'select'
else
  require 'nio4r_ext'

  if defined?(JRUBY_VERSION)
    require 'java'
    require 'jruby'
    org.nio4r.Nio4r.new.load(JRuby.runtime, false)
    NIO::ENGINE = 'java'
  else
    NIO::ENGINE = 'libev'
  end
end

これを見ると、OSがWindowsだとJRubyで無い限りは、必ずPure RubyのNIO4Rが使われるようだ…。
コマンドプロンプトから実行した場合、エラーが出ずに正常動作するのは、C実装でなくPure RubyのNIO4Rが使われているからということになる。
それでは、MSYSのrxvtやCygwinで実行すると、何故、Pure RubyでなくC実装のNIO4Rが使われるのだろうか。
とりあえず、環境変数OSをコマンドプロンプトで確認してみると、値は以下のように表示された。

C:¥User¥heroween>echo %OS%
Windows_NT

同じようにrxvtとCygwinでも確認すると、

$ echo $OS
Windows_NT

やはり"Windows_NT"と表示される。
それでは、Vagrant実行時にどうなっているのかと、nio.rbに直接puts ENV["OS"]と書き込んで実行したところ、

# コマンドプロンプト
Windows_NT

# MSYSのrxvt
MINGW32_NT-6.1

# Cygwin
CYGWIN_NT-6.1

と、ターミナル毎に異なった値が出力された。
そこで、VagrantのShell実装コマンドを見てみると、以下のように環境変数OSが上書きされていることが分かった。

# Determine the OS that we're on, which is used in some later checks.
# It is very important we do this _before_ setting the PATH below
# because uname dependencies can conflict on some platforms.
OS=$(uname -s 2>/dev/null)

unameコマンドの値で環境変数OSから"Windows"が消えてしまったことで、C実装のNIO4Rが使われるようになっていた訳である。

ここまでで、コマンドプロンプトとMSYSのrxvtやCygwinで挙動が変わっていた原因は分かった。
それでは、DLLエラーを吐く根本的な原因はなんなのだろうか。

処理の実行に失敗しているnio4r_ext.soをobjdumpで確認してみると、

$ objdump -T nio4r_ext.so

nio4r_ext.so:     file format pei-i386

DYNAMIC SYMBOL TABLE:
no symbols


C:¥MinGW¥bin¥objdump.exe: nio4r_ext.so: not a dynamic object

と表示されたので、soファイル自体壊れている可能性もありそうなのだが、Windows環境とC言語に明るくない自分では、究明するまでにかなりハマりそうなので、ここで打ち切ることにした。

対応

対応として、環境変数NIO4R_PUREに真の値を設定しておけば、必ずPure RubyのNIO4Rを使うようになるので、この問題を回避することはできる。

nio.rbのコードを見ると分かるが、そもそも、OSがWindowsの場合はC実装のNIO4Rを使わないような条件になっているし、過去のIssueやPRを追ってみても、単純にサポートしてないだけのように思われる。
どうしてもWindowsでC実装のNIO4Rを使いたいという訳でも無いので、一先ずはここまでにしておく。