CUBE SUGAR CONTAINER

技術系のこと書きます。

リモートサーバ上の Docker コンテナに X11 Forwarding する

リモートにあるサーバで動かしている Docker コンテナ上の X アプリケーションの GUI をローカルのマシンから確認したいと思った。 そこで、Docker コンテナとローカルマシンの間で X11 Forwarding してみることにした。 やってみると意外と手間取ったので記録として残しておく。

まず、リモートの Docker ホスト (Docker コンテナを動かすマシン) の環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-62-generic

そして、ローカルのマシン環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G95

ローカルの環境には、あらかじめ XQuartz (macOS 向けの X Window System 実装) をインストールしておく。

$ brew cask install xquartz

今回進める作業の手順としては、次の通り。

  1. リモートのマシン上で SSH でログインできる Docker コンテナを動かす
  2. ローカルのマシンから Docker コンテナに X11 Forwarding を有効にして SSH でログインする
  3. Docker コンテナ上で X アプリケーションを実行する
  4. 実行結果をローカルマシン上で確認する

リモートのマシンに Docker をインストールする

まずは最初の手順としてリモートのマシンに Docker をインストールする。

APT のリポジトリを更新した上で、もし既にインストールされている Docker コンポーネントがあればアンインストールしておく。

$ sudo apt update
$ sudo apt -y remove docker docker-engine docker.io containerd runc

続いて、Docker 関連のコンポーネントをインストールするのに必要なパッケージをインストールする。

$ sudo apt -y install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common \
    python3-pip

APT に Docker のリポジトリを登録する。

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

Docker 本体をインストールする。

$ sudo apt update
$ sudo apt -y install docker-ce docker-ce-cli containerd.io

次のように docker version コマンドでクライアントとサーバの両方が正しく出力されれば上手くいっている。

$ sudo docker version
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:29:11 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.8
  Git commit:       6a30dfc
  Built:            Thu Aug 29 05:27:45 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

続いて Docker Compose をインストールする。

$ sudo pip3 install docker-compose

こちらも、次のようにバージョン情報が出れば大丈夫。

$ docker-compose version
docker-compose version 1.24.1, build 4667896
docker-py version: 3.7.3
CPython version: 3.6.8
OpenSSL version: OpenSSL 1.1.1  11 Sep 2018

X11 Forwarding が有効な SSH ログインできる Docker イメージをビルドする

Docker がインストールできたら、次はコンテナのイメージを作る。

汎用性をもたせるためにプロセスは Supervisord 経由で起動するため以下の通り設定ファイルを用意する。

$ cat << 'EOF' > supervisord.conf
[supervisord]
nodaemon=true

[program:sshd]
command=/usr/sbin/sshd -D -ddd
user=root
stdout_logfile=/var/log/supervisor/sshd.log
redirect_stderr=true
EOF

コンテナが起動するプロセスとなるシェルスクリプトを用意する。 このスクリプトの中では SSH でログインするときのユーザのパスワード設定と Supervisord の起動を行っている。

$ cat << 'EOF' > docker-entrypoint.sh
#!/usr/bin/env bash

set -Ceux

: "Change user password" && {
  # check variable existence
  if [ ! -v PASSWORD_HASH ]; then
    echo "please define 'PASSWORD_HASH' environment variable"
    exit 1
  fi
  usermod -p "${PASSWORD_HASH}" ${USERNAME}
}

: "Start supervisord" && {
  supervisord -c /etc/supervisord.conf
}
EOF

続いて肝心の Docker イメージをビルドするための Dockerfile を用意する。 このイメージでは必要な設定を sshd に施すと共に、必要な関連パッケージをインストールしている。 ベースイメージは Docker ホストと同じ Ubuntu 18.04 LTS にした。

$ cat << 'EOF' > Dockerfile
FROM ubuntu:18.04

# Non administrative username (SSH login user)
ENV USERNAME=example

# Use mirror repository
RUN sed -i.bak -e "s%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g" /etc/apt/sources.list

# Install prerequisite packages
RUN apt update \
 && apt -yq dist-upgrade \
 && apt install -yq --no-install-recommends \
      sudo \
      xserver-xorg \
      openssh-server \
      supervisor \
      xauth \
      x11-apps

# Prepare OpenSSH server
RUN mkdir /var/run/sshd \
 && sed -i -e "s/^#AddressFamily.*$/AddressFamily inet/" /etc/ssh/sshd_config

# Setup non administrative user
RUN useradd -m ${USERNAME} \
 && usermod -aG sudo ${USERNAME}

# Expose container ports
EXPOSE 22

# Copy settings
COPY supervisord.conf /etc
COPY docker-entrypoint.sh /var/tmp

# Boot process
CMD bash /var/tmp/docker-entrypoint.sh
EOF

イメージとコンテナを管理するために Docker Compose 用の設定ファイルも用意しておこう。 この設定ではコンテナに SSH でログインする用の TCP/22 ポートを、Docker ホストの 127.0.0.1:2222 にマッピングすることになる。 意図的にループバックアドレスに bind するのは、下手にインターネットから到達できないようにするため。

$ cat << 'EOF' > docker-compose.yml
version: "3"
services:
  server:
    build:
      context: .
    environment:
      - PASSWORD_HASH
    image: example/sshd-container
    ports:
      - "127.0.0.1:2222:22"
EOF

Docker Compose 経由でイメージをビルドする。

$ sudo docker-compose build

上手くいけば次のようにイメージが登録される。

$ sudo docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
example/sshd-container   latest              0c9b2af9011d        19 minutes ago      318MB
<none>                   <none>              df1f957680d7        20 minutes ago      318MB
ubuntu                   18.04               a2a15febcdf3        4 weeks ago         64.2MB

コンテナを起動する

先ほど docker-entrypoint.sh の中でチェックしていた通り、コンテナの起動には SSH ログイン用のパスワードハッシュが必要になる。 例えば次のように Python などを使って生成しておく。

$ python3 -c "import crypt; print(crypt.crypt('mypasswd', crypt.METHOD_SHA512))"
$6$ADfMjPqRhaGQ4r80$Pc4n29qZmdRFMJBGK1FghsmWWQZOqgjom8ia2SA5r9HeCnhASKUoToq30Q5eCh3272/UvvaVfGa0IIpGyOwbK1

パスワードをターミナルの履歴に残したくないときは次のように getpass.getpass() を使うと良いかも。 あるいは、もっとちゃんとするならパスワード認証ではなく公開鍵を仕込むようにした方が良いと思う。

$ python3 -c "import crypt; from getpass import getpass; print(crypt.crypt(getpass(), crypt.METHOD_SHA512))"

生成したパスワードのハッシュを環境変数ごしに渡しつつコンテナを起動する。

$ export PASSWORD_HASH=$(python3 -c "import crypt; print(crypt.crypt('mypasswd', crypt.METHOD_SHA512))")
$ sudo docker-compose run \
    --name sshd-container \
    --rm \
    -e PASSWORD_HASH=${PASSWORD_HASH} \
    --service-ports \
    server

これでコンテナに SSH でログインするためのポートがリモートマシンの 127.0.0.1:2222 にマッピングされる。

$ ss -tlnp | grep 2222
LISTEN   0         128               127.0.0.1:2222             0.0.0.0:*

X11 Forwarding が有効な状態で SSH ログインする

ここまでの工程でコンテナにアクセスするためのポートは開いた。 ただし、bind されているアドレスが Docker ホストのループバックアドレスになっている。 セキュリティを考えてのことだけど、このままだとローカルのマシンからも疎通がない。 この点は SSH の Port Forwarding で解決する。

リモートの 2222 ポートを、ローカルの 22222 ポートに Port Forwarding で引き出してくる。 例えば今回は Vagrant を使ってリモートサーバを構築してあるので、次のようにする。 新しいターミナルを開いて Port Forwarding しよう。

$ vagrant ssh-config > ssh.config
$ ssh -F ssh.config \
      -L 22222:localhost:2222 \
      default

これでリモートの 2222 ポートが、ローカルの 22222 越しにアクセスできるようになった。

$ lsof -i -P | grep -i listen | grep 22222 
ssh       35299 amedama    5u  IPv6 0x9d2c7f5e707aebd5      0t0  TCP localhost:22222 (LISTEN)
ssh       35299 amedama    6u  IPv4 0x9d2c7f5e74def915      0t0  TCP localhost:22222 (LISTEN)

あとはローカルの TCP/22222 に向けて X11 Forwarding を有効にしつつ SSH でログインする。 OpenSSH の実装であれば -X をつければ X11 Forwarding が有効になる。

$ ssh -XC \
      -o StrictHostKeyChecking=no \
      -o UserKnownHostsFile=/dev/null \
      -p 22222 \
      example@localhost

ログインすると、ちゃんと DISPLAY 環境変数の内容もセットされている。

$ echo $DISPLAY
localhost:10.0

例えばサンプルとして xeyes を起動してみよう。

$ xeyes

見慣れた目玉が見えるようになるはず。

f:id:momijiame:20190912235905p:plain

もし上手くいかないときは、次のようにして Supervisord が起動した sshd のログを確認すると良い。

$ sudo docker exec -it sshd-container tail -f /var/log/supervisor/sshd.log

いじょう。

参考

blog.n-z.jp