Ubuntu Weekly Recipe

第479回LXDコンテナとホストの間でファイルを共有する方法

LXDを使っていると非特権コンテナであってもホストとコンテナ間で気軽にファイルを共有したいことがよくあります。今回はその手順をまとめておきましょう。

LXDと非特権コンテナ

LXDはかんたんにシステムコンテナを構築できるツールです。⁠コンテナ型仮想化」と言えばDockerが有名ですが、これは主に「1コンテナ1サービス」というアプリコンテナとしての使われ方が主流です。それに対してLXDは「普通のLinuxシステム」をひとつのコンテナの中に構築します。どちらかと言うとKVMやVirtualBox、VMWareの代わりに使うタイプのソフトウェアです。

用途の違いからDockerとLXDは排他的ではなく、適材適所に使い分けることができます。もちろんLXDの上でDockerを動かすことも可能です。DockerとLXDの違いについては第459回にまとめていますので、そちらも参照してください。

LXDでは原則として「非特権コンテナ」を構築します。非特権コンテナではユーザー名前空間を用いて、コンテナとホストの間でUIDやGIDのマッピングを行います[1]⁠。このためコンテナ上のroot(UID=0)はホスト上ではUID=0以外のUID、つまりは一般ユーザーとして見えるのです。またそれ以外のコンテナの一般ユーザーはIDは別の値にマッピングされてシステムが起動することになります。

コンテナ上でのプロセス
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 Jun08 ?        00:00:12 /sbin/init
root        47     1  0 Jun08 ?        00:00:02 /lib/systemd/systemd-udevd
root        48     1  0 Jun08 ?        00:00:09 /lib/systemd/systemd-journald

ubuntu   29285     1  0 17:52 ?        00:00:00 /lib/systemd/systemd --user
ubuntu   29286 29285  0 17:52 ?        00:00:00 (sd-pam)
ubuntu   29334 29283  0 17:52 ?        00:00:00 sshd: ubuntu@pts/0

ホスト上でのプロセス
UID        PID  PPID  C STIME TTY          TIME CMD
165536    4288  4226  0 Jun08 ?        00:00:12 /sbin/init
165536    4496  4288  0 Jun08 ?        00:00:02 /lib/systemd/systemd-udevd
165536    4498  4288  0 Jun08 ?        00:00:09 /lib/systemd/systemd-journald

166536   12123  4288  0 17:52 ?        00:00:00 /lib/systemd/systemd --user
166536   12127 12123  0 17:52 ?        00:00:00 (sd-pam)
166536   12177 12120  0 17:52 ?        00:00:00 sshd: ubuntu@pts/0

ホスト上では、root(UID=0)がUID=165536、ubuntu(UID=1000)がUID=166536になっていることがわかります。また、プロセス名前空間も使っているためPIDもコンテナとホストでずれていることがわかりますね。このようにコンテナの内部のrootを含めたユーザーはホストから隔離されています[2]⁠。

実際にどの値にマッピングされるかは、デスクトップ版にあとからLXDをインストールするか、最初からLXDがインストールされているサーバー版を使うかで微妙に異なります。

  • デスクトップ版:コンテナ内部のUID/GIDの0から65535は、ホスト上では165536から231071にマッピングされる
  • サーバー版:コンテナ内部のUID/GIDの0から65535は、ホスト上では100000から165535にマッピングされる

本連載でも第475回第416回などで、ソースコードが明らかでないデスクトップ向けアプリのバイナリを隔離環境で動かす際に利用していました。ただしこのままだとコンテナ内部のアプリが作ったデータをホストに移動するのは大変ですし、逆も同じです。特に一般ユーザーのUIDとGIDが異なるため、単純に移動してもそのままでは変更できないことがあります。そこで今回はホストとゲストでシームレスにファイルのやり取りが行える仕組みを考えます。

LXDコンテナ環境の準備

まずはLXD環境を構築しましょう。LXDはUbuntu 16.04 LTS以降であれば簡単に構築できます。Ubuntu 16.04 LTSの公式リポジトリであれば2.0系、それより新しいリリースであればより新しいLXD 2.xがリポジトリに用意されているからです。また16.04のバックポートリポジトリから最新版をインストールできます。2.xのほうがいろいろと機能追加されていて便利なのですが、バージョンも頻繁にアップデートされるため、Ubuntuのサポートの範囲内で安定的に運用するなら2.0を使うことをおすすめします。今回の記事に関しては、2.0でも2.x系のUbuntuリポジトリ上の最新版である2.14でも動くように記述しています。

最初にLXDをインストールします。サーバー版のUbuntu 16.04 LTSであれば、最初からインストールされているはずです。明示的にインストールする場合は、次のコマンドを実行してください。

$ sudo apt install lxd

インストールと同時にlxdグループが作成され、sudoを実行できるユーザーは自動的にlxdグループに所属します。グループの変更を反映するために、一度ログインしなおしましょう。lxdグループに所属すると、sudoなしにlxcコマンドを実行できるようになります。

次にLXDの初期設定を行います。LXDはsudo lxd initによって、全体的な初期設定を行えます。設定するのはストレージのバックエンド(コンテナのルートファイルシステムをどのように保存するのか)とコンテナのネットワーク設定です。というわけで、インストール直後に一回だけ実行します。

$ sudo lxd init

基本的に既定の設定をそのままOKしていくだけです。詳しいことはUbuntu Weekly Recipeの第459回の2ページ目を参照してください。

次にUbuntu 16.04 LTSベースのコンテナ(コンテナ名:sample)を作ります。

$ lxc init ubuntu:16.04 sample
sample を作成中
$ lxc start sample
$ lxc exec sample -- sudo -iu ubuntu ssh-import-id lp:(Launchpadのアカウント)
$ lxc list
+---------+---------+--------------------------------+----------------------------------------------+------------+-----------+
|  NAME   |  STATE  |              IPV4              |                     IPV6                     |    TYPE    | SNAPSHOTS |
+---------+---------+--------------------------------+----------------------------------------------+------------+-----------+
| sample  | RUNNING | 10.101.57.183 (eth0)           | fd2f:cd:157d:2362:216:3eff:fe5e:6952 (eth0)  | PERSISTENT | 0         |
+---------+---------+--------------------------------+----------------------------------------------+------------+-----------+

これでコンテナは完成です。Ubuntuコンテナは最初からOpenSSHサーバーが動いています。ただしubuntuユーザーのパスワードがロックされているためそのままではログインできません。そこでssh-import-idコマンドで、コンテナ内部のubuntuユーザーのホームディレクトリにLaunchpadから公開鍵をダウンロードしています。

lxc fileコマンドを使ったファイルの受け渡し

ホストから特定のコンテナとのファイルを受け渡ししたいのであれば、lxc fileコマンドが使えます。

コンテナからファイルを取得する
$ lxc file pull コンテナ名/フルパス 保存先

コンテナにファイルを渡す
$ lxc file push ファイル コンテナ名/フルパス

気をつけなければいけないのは、コンテナ側のファイル名は「コンテナ名/フルパス」の書式であるということです。sampleコンテナの/etc/environmentならsample/etc/environmentになります。-rオプションをつけると、再帰的に受け渡しします。またcpコマンドなどと同じく送信元のファイル(=最後の引数を除いたファイル)は複数指定可能です。その場合、保存先はディレクトリになります。

lxc file pushの場合、-pオプションを付けることで保存先のパスに合わせてディレクトリを作成します。また--uid--gid--modeオプションによって保存先のファイルオーナーやモードを変更できます。

非特権コンテナであってもlxc fileコマンドを使う限り、UID等の変更は「うまく」扱ってくれます。コンテナ上のUID=1000(ホスト上のUID=166536)がオーナーのファイルをホストにpullしたら、ホスト上のUID=1000がオーナーになりますし、その逆もまた然りです。コンテナとホストの間を手動で受け渡しすれば事足りるのであれば、lxc fileだけで十分です。特に別ホストにいるコンテナ(リモートコンテナ)にも同じコマンドで送ることができるというメリットがあります。

ちなみにlxc fileにはファイルを削除するlxc file deleteやホスト上のテキストエディターでコンテナのファイルを編集できるlxc file editコマンドもあります。特に後者は、ちょっとコンテナの設定を書き換えたいときに真価を発揮します。

ホストのディレクトリーツリーをbind mountする

名前空間による制限により、コンテナからホストのルートファイルシステムを直接見ることはできません。またAppArmorにより、コンテナ内部からのブロックファイルのmountは禁止されています。もしコンテナとホストの間、もしくは同じホスト上のコンテナ間でファイルやディレクトリを共有したい場合は、コンテナの設定でbind mountする方法が便利です。

$ lxc config device add sample share disk source=/srv/shared path=/srv/shared

上記のコマンドはホストの/srv/sharedsource=オプション)を、コンテナ内部の/srv/sharedpath=オプション)としてbind mountします。つまりホストとコンテナで/srv/sharedを共有できるというわけです。

lxc configによる設定は永続的に反映されますので、コンテナの再起動を行ってもそのまま残っています。

$ lxc config show sample
(中略)
devices:
  nfsdir:
    path: /srv/shared
    source: /srv/shared
    type: disk
(後略)

この方法のメリットは、ホスト側のファイルシステムに関係なく同じ方法でコンテナと共有できることです。たとえばコンテナの内部からNFSディレクトリをmountする場合も、一度ホスト側でNFSディレクトリをmountしておけば、それをそのままコンテナにbind mountできるのです。よって同じホスト上のコンテナ間だけでなく、ネットワーク越しのコンテナ間ともファイルやディレクトリを共有できます。

ただしlxc fileと異なり、UID等のマッピングの変更は行いません。よってコンテナの一般ユーザーで作ったファイルやディレクトリのオーナーは、ホストから見るととても大きなUIDを持っていることになります。

ホストとコンテナでUID等を一致させる

bind mountによる共有ディレクトリを使う場合、ホストとコンテナで一般ユーザーのUID等を一致させたほうが便利です。つまりコンテナのUID=1000はホストでもUID=1000にします。もちろんrootを一致させるとそれはとどのつまり「ほぼ特権コンテナ」となりますので、非特権コンテナにしておく意味が薄れます。あくまで「普段使うユーザー」のみUIDを一致させるのです。理想的にはそのユーザーはsudoグループに入っていないほうがいいでしょう。

UID等のマッピングは/etc/subuid/etc/subgidファイルで管理しています。UID=1000、GID=1000のユーザーとグループはホストとコンテナで一致したい場合は次のように設定します。

$ echo "root:1000:1" | sudo tee -a /etc/subuid
$ echo "root:1000:1" | sudo tee -a /etc/subgid

このうちrootは今回設定したUID等のマッピングを許可するユーザーです。LXDの場合はrootがコンテナ起動時にマッピングを行うのでrootにしています。書式はUSER:START:COUNTです。上記設定の場合は、⁠ID=1000から1つ」になります。もちろん1100にして複数のUIDをマッピングすることも可能です。

コンテナ内部のマッピングに関する設定は、コンテナごとに行います。

$ lxc config set sample raw.idmap 'both 1000 1000'

bothはUIDとGID両方に同じ値を設定するパラメーターです。書式はboth HOST_ID CONTAINER_IDとなります。HOST_IDにはホスト上のUID/GIDを、CONTAINER_IDにはHOST_IDをコンテナの中にマップした時のIDを記述します。言い換えると「ホスト上のUID/GID 1000をコンテナ上のUID/GID 1000として扱う」ということです。

もしUID/GIDを異なる値にしたい場合はbothのかわりにuidgidを使います。

ホスト上のUID=1010をコンテナ上ではUID=1000として扱いたい場合
uid 1010 1000

ホスト上のGID=1011をコンテナ上ではGID=1000として扱いたい場合
gid 1011 1000

uidgidを同時に設定したい場合は、改行でつなぎます。しかしながらlxc configコマンドは直接改行を扱えません。そこで設定内容を標準入力から受け付けるようにして、そこにパイプで設定内容を流し込みます。

$ echo -e "uid 1010 1000\ngid 1011 1000" | lxc config set xenial raw.idmap -

ハイフンをつないで範囲を指定することも可能です。この場合、マッピング元とマッピング先の個数が同じになるようにしてください。

変更した設定を反映するには、一度そのコンテナを再起動します。冒頭と同じようにプロセスのUIDをコンテナとホストで比べたら、UID=0はマッピングされているものの、UID=1000はコンテナとホストで一致していることがわかるはずです。

これらを組み合わせれば、ホスト・コンテナ間のファイルやディレクトリのアクセス権をそれなりにコントロールできることでしょう。

おすすめ記事

記事・ニュース一覧