DockerコンテナdeGUIアプリ
そういえばDockerの中でGUIアプリケーションって動くのかな?
とふと思いました。
やってみるか…。
LinuxのGUI関係の知識が乏しいので前途多難感がものすごいですが、勉強になるし、密かに抱いているDockerで開発環境を作り上げるたいという野望にも関係してくるかもだし。
ターミナルエミュレータやVim、Emacsをコンテナに分離するだけでなく、IDEやCIツール、テスト環境、実行環境などをすべてDockerで作れたら、プログラミング言語も多数扱うのが楽な開発環境になるんじゃないだろか…。
今回はそんな野望のための第一歩として勉強します。
事前に調べてみたところDockerコンテナからGUIアプリを立ち上げることは可能なようです。
が、結構苦労しました。
では顛末をお楽しみください。
DockerコンテナからGUIアプリケーションを立ち上げる
まずはコンテナの用意
今回の挑戦にちょうどいいDockerイメージを探したんですがなかったので、Dockerコンテナを作るためのdockerfileを作りビルドすることにしました。
debianに若干慣れているので、ベースはdebianにしました。GUIアプリケーションはとりあえずターミナルエミュレータであるxtermを入れてみます。
FROM debian:8.6
RUN apt-get update
RUN apt-get install xterm -y
CMD ["/bin/bash"]
Dockerコンテナにインストールされたxtermが、Dockerホスト側でウインドウとして起動できたら成功…ということデス。
こいつは事前にビルドしておき、いつでもコンテナを作れるようにしておきます。
[mino@unskilled]$ docker build -t mino/debian-xterm /hoge/dockerfile
あ、ちなみにホストはArch Linux(ウインドウマネージャはi3-wm)です。
X Window Systemについて
さて、@MINOはX Window Systemの知識があまり(ほとんど)ありません。よって困難極まりない気がしています…。
とりあえずウインドウを起動するというのはどういうことなのだ?
いろいろ調べてみましたが、以下のようなことがわかりました。
Xorg
X window system Ver.11は通称X11などとも呼ばれています。
その実装がXorgということですが、Xorg以外の選択肢がほとんどなく、実質的にLinuxではXorgがGUI環境構築のスタンダードになっています。
X Window Systemはサーバ・クライアント式のシステムとして動いているそうです。
GUIに関する様々な挙動の大本はXサーバが取り仕切っていて、GUIアプリケーションはXクライアントとしてXサーバと通信をして動いているのだそうです。
UNIX系OSは予め多人数で使うように設計されているのでその方が都合がいいのですね。
ウインドウを生成しているのは、Xサーバではなくて、これまたXクライアントになるそうですが、ウインドウマネージャが行っています。
ウインドウマネージャはGUIアプリケーションの起動場所を提供するクライアントなんですね。
XサーバとXクライアントはXプロトコルで通信を行っているそうです。
そして、XサーバとXクライアントを通信させる方法はいくつかあるそうです。
有名なのはsshを使ったX11forwarding(転送)です。リモート(遠隔のサーバとか)で起動したXクライアントを、クライアント(手元のパソコンとか)側のXサーバと通信させて、ウインドウとして表示させることができます。
ただ今回は、Dockerコンテナなので、SSHを使うのはちょっとふさわしくないかもしれません。
UNIXドメインソケットを共有することでXサーバと通信する
いろいろ調べていくうちに「ソケット」を共有することで通信を行うという方法があることがわかりました。
LinuxではUNIXドメインソケットというものをつかって、プロセス間で通信を行う場合があります。UNIXドメインソケットは「ドメイン」という名前が表すとおり、単一のOSの中での通信をサポートします。
ソケットはいわばファイルで、通信はそのファイルを介して行われることになります。
つまりXサーバが使用しているソケットファイルをDockerコンテナから使うことができれば、DockerコンテナからGUIアプリケーションを動かせる可能性があります。
Xサーバが使用しているソケットファイルは以下にありました。
/tmp/.X11-unix/
このディレクトリをlsで確認してみると、一つファイルらしきものが入っているのがわかります。
[mino@unskilled]$ ls -al /tmp/.X11-unix/
合計 0
drwxrwxrwt 2 root root 60 12月 6 10:35 .
drwxrwxrwt 10 root root 240 12月 6 16:21 ..
srwxrwxrwx 1 root root 0 12月 6 10:35 X0
でもパーミッションの先頭に「s」ってありますね。どうやら普通のファイルではないことがわかります。
ご想像の通り、この「s」は「socket」のsだそうで、ソケットファイルであることを表します。ですから「X0」というのがXサーバで使われているUNIXドメインソケットなんですね。
このソケットをDockerコンテナに取り込むことでXサーバとの通信ができるはずです。
取り込み方としてはDockerのボリュームを使うのが一番簡単なようです。
コンテナにボリュームをバインドするときはdocker run時に-vオプションで”ホストのディレクトリ:コンテナのディレクトリ”として指定します。
環境変数DISPLAY
また通信相手のXサーバを特定するために環境変数のDISPLAYをコンテナの中に引き入れる必要があるようです。
ホスト側でenvコマンドを実行するとDISPLAYを確認することができます。
[mino@unskilled ~]$ env
LANG=ja_JP.utf8
GDM_LANG=ja_JP.utf8
DISPLAY=:0
XDG_VTNR=7
XDG_SESSION_ID=c2
XDG_GREETER_DATA_DIR=/var/lib/lightdm-data/mino
XTERM_SHELL=/bin/bash
~~~~~~
~~~~~~
~~~~~~
このDISPLAYは一つのOSの中で起動しているXサーバが担当するディスプレイの識別番号だそうです。
:0(デスクトップ機の場合は:0.0となっているかもしれません)と短い内容ですが、これは省略がされているようでして本来は
ホスト:ディスプレイ.スクリーン
という形式になるそうです。
ホストは物理的にディスプレイが接続されているマシンを表します。localhostやIPやホスト名でマシンを特定することができます。
localhostの場合、本来はlocalhost:0となりますが、:0のように空白の場合はlocalhostという意味になるそうです。
次のディスプレイですが、これは複数のディスプレイが接続されていることがある場合の識別番号です。
これは物理的なディスプレイというよりも、たとえばデュアルディスプレイは2つの物理ディスプレイを1つのディスプレイとして使いますので、それの場合はまとめて一つのディスプレイとして数えられます(はずです)。この番号は省略できません。
スクリーンはこれもデュアルディスプレイ等の複数ディスプレイを一つで使う際のそれぞれのディスプレイを識別するための識別番号です。
つまりひとつのディスプレイとしてつかっているデュアルディスプレイのそれぞれのディスプレイ(ややこやしい)のことです。実質的にはこれが物理ディスプレイを表しているということになるかと思います。
これも空白の場合は1番目のディスプレイ(数字としては0)ということなり、省略が可能です。
なので本来:0は
localhost:0.0
という意味なんですね。ですからこのDISPLAY変数で通信すべきホストとか、さらに表示すべきディスプレイもわかるということになります。
このDISPLAY変数をコンテナにも環境変数として引き込みます。
コンテナに環境変数を設定するときはdocker run時に-eオプションで指定します。
ソケットとDISPLAY変数をバインドしたコンテナの起動
以上のことを踏まえてコンテナを起動してみたいと思います。
コマンドは以下のようになります。
[mino@unskilled]$ docker run -it --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix mino/debian-xterm
コンテナをいちいち消したりするのが面倒なので(たくさん試行錯誤するので)、コンテナ落としたらコンテナ削除のオプションである–rmを付けています。なのでコンテナ名つけてません。
-e DISPLAY=$DISPLAY
の部分でホストのDISPLAY変数をコンテナのDISPLAY変数に入れています。$DISPLAYでホストのDISPLAYを展開しています。
-v /tmp/.X11-unix/:/tmp/.X11-unix
がソケットが入ってるディレクトリを同名同構成のディレクトリとしてコンテナとバインドしています。(おそらくなんですが、XサーバのUNIXドメインソケットの場所はクライアントが知っている必要があるので、正規の場所じゃないとだめなんだと思います)
xtermは起動するか…?
さて、以下のようにコンテナが起動しますので、xtermを起動してみます。
root@7cb9436bbe02:/#
しかし、xtermは起動してくれません。エラーになります。
root@56f995edcf4:/# xterm
No protocol specified
Warning: This program is an suid-root program or is being run by the root user.
The full text of the error or warning message cannot be safely formatted
in this environment. You may get a more descriptive message by running the
program as a non-root user or by removing the suid bit on the executable.
xterm: Xt error: Can't open display: %s
割と長めのエラーメッセージがでました。
超意訳です。
プロトコルが指定されとらん
警告:お前rootでこのプログラム動かしているだろ。あぶないぞ。通常ユーザで動かせ。
xterm: Xt error: ディスプレイが開けん: %s
うーむ。
ここでかなり煮詰まりました。いろいろ調べましたがコンテナ側に設定したソケットや環境変数には問題がないようです。
何かが足りないのか…。rootだとだめなのか?ディスプレイが開けないとは?
認証する必要があった
X Window Systemについてググっていたところ、認証に関して知ることが出来ました。
どうもXクライアントがXサーバと通信するには認証が必要な模様です。
確かにそうしないとどこからでも繋げちゃうので危険ですよね。
で、その認証はXサーバ側で「どのホストを認証する」というように許可をしなければならないようです。
どうもホスト側にも何かの設定をする必要がありそうです。
xhost
そこでさらに色々調べているうちにxhostなるコマンドについて知ることができました。
こんな方法でXクライアントの通信を許可できることがわかりました。
$ xhost +
これはすべてのクライアントから通信を受け付けるという設定になります。
ただ注意したいのが、これは認証をしているのではなく、単にザルになったというだけです。有象無象をすべて受け付けるお、ということです。
もちろんこれはセキュリティ的によくないですし、複数のドキュメントで「安全ではないよ」と書かれていました。
これはアカン。まずは少なくとも全許可状態はなんとかしないと。
そこで許可する範囲を狭めるために、DockerコンテナのIPにて許可する設定をすればいいのではないかと思いつき、コンテナのIPを調べて設定してみました。
まずコンテナのIPを調べます。
$ docker network inspect bridge bridge
~~~~~
~~~~~
"Containers": {
"d633ce291a6c871fba0ef76615b55860bd24885c531c4c708c3953cad2066b6c": {
"Name": "distracted_swanson",
"EndpointID": "1345ac9c26243a99e2d62524bed3559c19eda5d0249221a4a4e15140e49b5a4d",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
~~~~~
~~~~~
IPv4Addressの箇所がコンテナのIPです。172.17.0.2だとわかりました。
そして許可してみます。
このときxhostは現在のXのユーザのままで実行してください。rootになったりsudoを使う必要はありません。
$ xhost inet:172.17.0.2
172.17.0.2 being added to access control list
さて、どうでしょうか?
…しかし、Dockerコンテナからxtermは起動出来ず。先程と同じエラーがでちゃいます。
root@56f995edcf4:/# xterm
No protocol specified
Warning: This program is an suid-root program or is being run by the root user.
The full text of the error or warning message cannot be safely formatted
in this environment. You may get a more descriptive message by running the
program as a non-root user or by removing the suid bit on the executable.
xterm: Xt error: Can't open display: %s
あれなんでだろう。おかしいな。こわいな。IPじゃだめなんか?
数時間ほど悩んでいました。
ソケットはローカルのものを使っているんだぜ
ハッピーターンを食べながら冷静になって考えてみれば、ソケットは「共有している」形になっているのですから、ローカルからの接続になるんじゃね?と気が付きました。
すこしややこしいですが。Dockerコンテナ自体はもちろんホストとは別の仮想マシン(といっていいの?)として動いています。
しかしボリュームで共有したソケットはホストが持っているものです。つまり通信相手はネットワーク越しではなくローカルということになります。
ですからコンテナのIPを許可しても意味が無かったのです。もともとUNIXドメインソケットはローカルの中で使われるものですしね。
そこでxhostのmanページを確認してみるとlocalhostの指定の仕方が書いてありました。
A complete name has the syntax “family:name” where the families are as follows:
~~~~~~
~~~~~~
local contains only one name, the empty string
~~~~~~
~~~~~~
localの場合は空白でいいようですので、以下のようになります。
$ xhost local:
xtermが起動した!!
実行後、コンテナからxtermを起動させてみると…
テッテレー!!
ホストのウィンドウとしてコンテナのxtermが起動してくれました。やったぜ!!
xhostについて補足
xhostのmanページにあった記述です。
The local family specifies all the local connections at once
「ローカルからの接続は全部許可するよ」という意味みたいなのですが、でもローカルからの接続ってもともと許可されているものではないのかな?
だってUNIXドメインソケットを使った通信はネットワークを跨げないので、どちらにしてもlocal通信になりますよね?
ホストのアプリケーション達はXサーバとどうやって通信しているのでしょうか?
UNIXドメインソケットを使っているのだとしたらローカルは受け付けていないと通信ができないのでは?
しかし色々調べているうちにXサーバはホストのみならずユーザに関しても認証が必要のようです。
確かに、得体のしれないユーザがXクライアントを起動できるというのはセキュリティ的にあやういですね。
このことは以下のサイトさんに記述がありました。
いますぐ実践! Linux システム管理 宿題の答え
http://www.usupi.org/sysad/265.html
Xでは、ホストとユーザ双方とも、アクセス制御がかかっています。
UNIXドメインソケット経由という時点で、ホストは許可されているのですが、 ユーザは許可されていません。
Dockerコンテナ内のユーザは、ローカルに存在するユーザではないということになります(よね?)
Dockerコンテナは普通root状態ですが、このrootはもちろんホストのrootとは別ものです。
ですのでXとしてはDockerコンテナがUNIXドメインソケットを使って通信してきても「こいつは知らんユーザだな」ということになるようです。
xhostはホスト単位でザル状態を作り出すので、先程の「xhost local:」方法であればlocalのどのホスト(という言い方でいいのかな)でも接続できるザル状態になります。
もう少しセキュアなxhost
上記のサイトさんでも書かれている通り、xauthを使うほうがセキュアです。しかし認証を得るためにはコンテナからホストに入ってxauthを実行する必要がありますので、これにはコンテナにssh等を入れないといけません。
sshを使わずにクッキーを作ってコンテナに入れるという方法も試したのですが、どうもうまく行かず認証がされませんでした。
そこでもう少し簡単な方法は無いだろうかといろいろ調べていると、xhostにてホスト名単位で許可ができることもわかりました。
以下の様にすることで、指定したローカルの合致するホスト名だけをザルにすることができます。
$xhost local:ホスト名
つまりDockerコンテナであれば以下の様な感じになるわけです。
$xhost local:df26e667ffd3
df26e667ffd3ってのはdockerコンテナのホスト名のことです。
しかしこれだといちいちコンテナのホスト名を調べるの面倒ですね…。
というわけで、ここはコマンド展開を使いましょう。docker inspectのテンプレートを使ってhostnameを取得します。
それでもコンテナIDは調べなきゃいけないですね…。
シェル上でバッククオートはコマンド実行を意味しますので、以下はdocker inspectの結果(コンテナのホスト名)が入ります。
xhost +local:`docker inspect --format='{{ .Config.Hostname }}' [コンテナID]`
docker start [コンテナID]
これでlocalのホストすべてを許可するというザル具合から、特定のコンテナだけを許可することができます。
おそらくそれなりにセキュリティは保てるのでは無いかと思います(が自信ないです)。
先程xhostで
$xhost +
$xhost local:
を行っていた場合は、閉じる必要がありますので、
$xhost −
と
$xhost −local:
を念のために行ってください。
これらのザル設定が生きていると関係なしに通ってしまいますからね。
まとめ
なんとかDockerコンテナ内のGUIアプリケーションを起動することができました。かなり初歩的なテストでしたが、Dockerをいろいろ活用するための一歩となりました。
これでいろんなGUIアプリケーションをDockerコンテナで隔離した状態で起動することができ、dockerfileで管理できるようになります。
開発環境を構築する選択肢も広がった気がします。
さらにXやDockerの勉強して、よりセキュアで管理しやすいコンテナ開発を続けていきたいと思います。
もしTIPSが出来たらまた記事にしたいと思います。