NeetEch

X11とDockerを少し(でも)理解した(い)

こんにちは,ぎいとです.

以前PipenvをDockerコンテナ内に閉じ込めるという記事を書いたのですが,やはりpoetryを使うのが正解っぽいので移行することに.

構想のベースは以前の記事と同様.gitやvimなんかの操作をホストOSで,pythonに関連する操作をコンテナの中で行う開発環境の構築です.が前回と同じでは色々不都合があったのでここで解決してしまおうというのが始まり.

poetryに移行するとは言ってもこの記事でpoetryはほとんど出てこないです.一応イメージは公開してありますが,あまりいいやり方ではないと思うので改善したものを後で公開できたらいいなと思ってます.

Macをご利用の方には役に立たない記事です.Macの場合そもそも権限問題が起こらないのとX11を利用していないので.

ということで.

長い前書き

前書きがいつもどおりグダグダと長いです.要約すると,「権限どうなってんねん」と「X Window Systemがわからない」です.

まずは問題点の整理

問題点1.権限がめんどくさい,そして怖い

Dockerコンテナは特に何も指定しなければユーザはrootのみ,コンテナ内では全てroot権限での操作となります.簡単な実験を行うとコンテナ内で作成されたファイルやディレクトリはrootの管理下にあることが確認できます.

#  一般ユーザでログイン中
$ mkdir TEST && cd TEST
$ ls -lA
total 0

#  テストファイル,ディレクトリの作成
$ docker run --rm -v ${PWD}:/root centos touch /root/test.txt
$ docker run --rm -v ${PWD}:/root centos mkdir /root/test
$ ls -lA
total 4
drwxr-xr-x 2 root root 4096 5月 21 11:26 test
-rw-r--r-- 1 root root    0 5月 21 11:26 test.txt
#  一般ユーザからの操作は限定される
$ echo 'foo' >> test.txt
bash: test.txt: Permission denied
$ mkdir test/bar
mkdir: cannnot create directory 'test/bar': Permission denied

通常ならホストOS上で作成したディレクトリをコンテナ上にマウントするので,このような権限問題は限定的です.しかし,pipefileやpyproject.tomlのように「コンテナ内で作成されホスト側でも編集したいファイル」は作られたそばからchownといった対処法を取らなくてはいけません.

余談

セキュリティの話.dockerコマンドでsudoが必要になるのは煩わしいという理由でdockerグループにユーザを追加している人は多いかと思います.ちょっと注意が必要ですので気をつけて.
先ほどroot権限で作成されたtestディレクトリに対する以下の操作を見てみます.

#  上の続き
$ echo 'hogehoge' > test/hoge.txt
bash: test/hoge.txt: Permission denied

$ docker run --rm -v ${PWD}:/root centos /bin/bash -c 'echo hogehoge > /root/test/hoge.txt'
$ cat test/hoge.txt
hogehoge

このコマンドはどちらもtest/hoge.txtにhogehogeの出力を行います.一般ユーザにはtest/への書き込み権限がないため失敗,ところがdockerコマンドでは書き込みができてしまいます.
やりようによってはroot権限がなくても色々できそう.Dockerと権限については慎重になった方がいいかもしれません.

問題点2.matplotlibは普通に出力できない

この話にはX Window Systemが入ってきます.Linuxで基本的に利用され,X11と呼ばれるらしいです.ややこしいのでかなりざっくり説明します.というか筆者もざっくりとしか理解できていないのであまり説明しません.

普段見ているウィンドウは,画面への出力とマウスやキーボードからの入力を制御するXサーバと,入力によって出力内容を制御するXクライアントが通信することで実現しています.

X11 イメージ図

テキトーにやっても通信の接続がうまくいかずmatplotlibで表示したい内容をホストに飛ばすことができません.

DockerはLinuxに基づいていますよね.コンテナ内のアプリケーションは基本的にこのX11で描画しようとするみたいです.つまりコンテナ内ではXクライアントが頑張ります.ということでXクライアントと通信が可能なXサーバをホストで用意してあげればいいのではないか,という感じですが果たしてどうなるんでしょうか.

先に述べておきますが,筆者はこの問題をDocker内のユーザ権限とDockerのネットワーク設定で解決しましたが,よく理解できていません.それもOSはLinuxのみ解決可能でMacでの実現は謎のまま.XQuartzとsocatを用いる方法とか,ssh接続してなんたらみたいな方法くらいしかわかりません.

これらの問題を解決しようとした筆者のまとめ記事ですが,多分もっと良い解決方法があります.それを知りながらこの記事を書いているので弱腰での説明です.理解ができたらまた書き直したい.
とりあえずの進捗というか,実験結果のまとめとして読んでください.こういうやり方もあるよね程度に参考になれば幸いです.

はい,前置き終わり.

コンテナにユーザ作ったらとりあえず解決する

いきなり結論ですが,コンテナの中にユーザ作ったらとりあえずうまくいきました.Ubuntu 20.04で実行してます.

poetryへの移行が主題ではあったものの,要はコンテナ内での権限とコンテナ内で実行されるGUIアプリケーションがホスト側で出力できればいいのでFireFoxで実験してみます.

FROM centos

RUN yum update -y && \
    yum install -y firefox && \
    yum clean all

ARG HOME=/root
ENV HOME=$HOME
WORKDIR $HOME

ARG UID=1000
ARG GID=1000

RUN groupadd user -og $GID && \
    useradd --uid $UID --gid $GID --no-create-home user && \
    chmod 755 $HOME && chown user:user $HOME
USER user

CMD ["/bin/bash"]

CentOSでやってみましょう.DockerfileではFireFoxのインストールとユーザの作成,権限の変更などを行なっています.

このDockerfileより作成されるイメージをcentos:testとしておきます.

$ docker build -t centos:test .

権限はゴリ押しで解決

ユーザid,グループidはホストOSの一般ユーザであるgilitoのidをそのまま利用しています.こうすることでDockerコンテナ内で作成されるユーザ,userはgilitoと同一と見做されるようです.

$ docker run --rm -it -v ${PWD}:/root centos:test

#  コンテナのユーザ,userとして作業
$ echo foo > foo.txt
$ mkdir bar
$ ls -l
drwxr-xr-x 2 user user 4096 May 21 06:59 bar
-rw-r--r-- 1 user user    4 May 21 06:59 foo.txt
$ exit

#  ホストの一般ユーザ,gilitoとして作業
$ ls -l
drwxr-xr-x 2 gilito gilito 4096 5月 21 15:59 bar
-rw-r--r-- 1 gilito gilito    4 5月 21 15:59 foo.txt

権限などの情報がidによって管理されていることが伺えます.

とりあえず権限問題が解消されることは確認できますが,Dockerfileの記述が面倒です.別のユーザが利用する場合にはUID,GIDを変更後,Dockerfileからビルドし,イメージを作成する必要があります.

また,Dockerfileで行なっているように,コンテナ内での権限についても気を使わなくてはいけないのも面倒です.コンテナ内の$HOME=/rootの「権限と所有者」をuserへと変更するのは,そもそもコンテナ内でのuserに/rootへの書き込み権限がないと話にならないからです.権限か所有者のどちらかを適切に変えれば済みますがどちらも変えてみました.どちらにせよ面倒ですよね.

今見返してみてもあまりいい解決方法ではないと思います.

参考: dockerコンテナ内のユーザとホストのユーザとの関係

FireFoxを起動したい

次にFireFoxを起動してみます.普通に起動しようとするとまぁ失敗します.

$ docker run --rm centos:test firefox
Failed to open connection to "session" message bus: Unable to autolaunch a dbus-daemon without a $DISPLAY for X11
Running without a11y support!
Unable to init server: Broadway display type not supported:
Error: cannot open display:

とまぁこれではうまくいかないのですが,次のようにするとうまくいきます.

$ docker run --rm --net host -e DISPLAY=$DISPLAY centos:test firefox

起動が確認できたでしょうか.追加したのは[ –net host ]と[ -e DISPLAY=$DISPLAY ]です.見ていきましょう.筆者の理解はかなり曖昧なので間違っていたらご指摘頂けると助かります.

なにが起こっているのか

冒頭でも触れたとおり,結局ホストのXサーバと,コンテナのXクライアント(FireFox)が通信できればいいはずです.実はそうでもないんですがそういうことにしといてください

X11の通信

コンテナどうこうの前にホスト上でアプリを起動したとき,どのようにしてウィンドウが表示されるのかを簡単に整理しておきましょう.かなり単純化し正確でない表現を多々含みますので注意です.

そもそもホスト上で起動するXクライアントはどこに情報を送っているのでしょうか.実はこれがDISPLAYという環境変数で表されているようです.

$ printenv DISPLAY
:0

確認してみるとDISPLAYという環境変数が用意されていますね.かなり省略されているのでわかりにくいのですが,省略せずに書くと “localhost:0.0” と表されるみたいです.”ホスト名:ディスプレイ.スクリーン”らしいのですが,重要なのはホスト名であると考えていいでしょう.ホスト名はネットワーク上の名前に対応します.

Xサーバは6000番のポートをlistenしているらしく,6000番のポートに入ってきた情報はXサーバが処理してウィンドウに表示してくれます.ホスト名がわかってしまえば,6000番ポートに流すだけですね.多分そんなに単純じゃないけどイメージはこんな感じだと思います.

ホスト上でFireFoxを起動した場合,ウィンドウが完成するまでの流れは,

「FireFoxを起動」→「ネットワーク上の通信相手(=自分)を見つける」→「6000番ポートに表示したいデータを送信」→「Xサーバが描画」

みたいな感じになると思います.これを踏まえてコンテナ上でFireFoxを起動した際の流れを追いましょう.

コンテナとネットワーク

dockerのコンテナとホストは通常同じ(ような)ネットワークに属しています.よってホストのIPアドレスをDISPLAY変数に含めて,コンテナに渡せば通信が可能になるはずです.
IPアドレスは,機械にとってわかりやすいホスト名といったところ.ホスト名の代わりにIPアドレスでの指定が可能です.

ところがこれも失敗します.原因はX11の認証です.Xサーバは知らない人からの通信を拒否する設定になっています.この時認証されるのはホスト名(IPアドレス)とユーザID(Dockerfileで揃えた).コンテナはよくも悪くもホストから隔離されているため,ホストからは知らない人認定されてしまったのです,悲しいですね.

ここで登場するのが–net hostです.–netはコンテナが利用するネットワークの設定で,hostを指定すると,IPアドレスやポートをホストとコンテナで共有するようです.ネットワークという観点ではホストとコンテナが同一人物として扱えるという認識でいいと思います.

コンテナとホストを同一人物として扱えれば簡単です.認証の必要もないし,IPアドレスをわざわざ渡す必要もなく,DISPLAY=:0で済むでしょう.

さて長くなりましたがこれが[ –net host ]と[ -e DISPLAY=$DISPLAY ]でコンテナ上のFireFoxが起動できる(テキトーな)理由です.そしてお察しのとおり,ホストがX11を利用していなければうまくいきません.よってMacなどではやはり機能しません.

参考:
https://sites.google.com/site/teyasn001/ubuntu-12-10/huan-jing-bian-shudisplay
dockerコンテナの中でguiアプリケーションを起動させる

別の解決方法

一応最後に紹介しておくと,LinuxのKernelには名前空間(namespace)と呼ばれる機能(?)があります.これをうまいこと使うと,ホスト上のユーザとコンテナ内のrootをマッピングすることが可能らしいです.

つまりわざわざユーザを作成して権限をいじるDockerfileを用意してビルドなんて面倒なことがいらないんですよね.これについてはまだまだ勉強が足りないのでまたの機会に.

同様にネットワークやプロセス間通信にも名前空間が存在しているらしいです.これDockerと関連がかなり深いと思うのに全然知りませんでした.勉強が足りなさすぎる.

参考: カーネルとか弄ったりのメモ

今日のちなみに

元々やりたかったpoetryの移行もこの記事の方法で実行.難なく成功したかに思えたがpoetry shellが実行できない

$ docker run --rm -it -v ${PWD}:/root/project poetry shell
Spawning shell within /root/project/.venv

[RuntimeError]
Unable to detect the current shell.

「あれ,なんでできないんだろ〜」と思ったら次のようにしてできた.どうなってんだ,考えてみる.

$ docker run --rm -it -v ${PWD}:/root/project --entrypoint /bin/bash poetry
user@7c8acdda40b:~/project$ poetry shell
Spawning shell within /root/project/.venv
user@7c8acdda40b:~/project$ . /root/project/.venv/bin/activate
(.venv) user@7c8acdda40b:~/project$ 
#  できてる

コマンドが2回打たれているような見え方だが,実際には$ poetry shellしか打っていない.
続く ‘. /root/project/.venv’ は勝手に実行されている(ように見せかけている)みたいです.

この挙動からpoetry shellコマンドは一度このコマンドのプロセスを終了させ,親プロセス(/bin/bash)から,.(source)コマンドを新たに実行させるという仕組みなのではないかということが予想できます.

実際 .venv/bin/activateには環境変数の定義やらが書かれていることが確認できました.

最初の失敗の理由は,poetry shellに親プロセス(/bin/bashとか)が存在しなかったためと推察できます.
ところが実際にはsourceコマンドなんてコンテナ内に存在していない.もう意味がわからん.

こういうときにソースコード眺めて理解できたらかっこいいですよね.眺めてみたけどpythonの知識が貧弱過ぎた.まぁそれはこれから頑張っていくさ.

というわけで今日はこの辺で勘弁してください.X Window Systemはいつか理解してリベンジしたい.

Follow me!