動作環境

必須の環境:

  • C ライブラリとして glibc、musl libc、uclib、bionic のいずれか
  • Linux カーネル 2.6.32 以上

lxc-attach の動作に必要な環境:

  • Linux カーネル 3.8 以上

非特権のコンテナが動作するのに必要な環境:

  • libpam-cgfs 非特権の cgroup 操作を行うためにシステムを設定する PAM モジュール
  • newuidmap、newgidmap を含む最新バージョンの shadow
  • Linux カーネル 3.12 以上

推奨ライブラリ:

  • libcap (ケーパビリティを落とすために必要)
  • libapparmor (コンテナに対して独自の apparmor プロファイルを設定するために必要)
  • libselinux (コンテナに対して独自の SELinux コンテキストを設定するために必要)
  • libseccomp (コンテナに対して seccomp ポリシーを設定するために必要)
  • libgnutls (色々なチェックサム確認に必要)
  • liblua (lua バインディングに必要)
  • python3-dev (python3 バインディングに必要)

インストール

通常はあなたがお使いのディストリビューションが、ディストリビューションのパッケージリポジトリもしくはバックポート用のチャンネル経由で、最新版の LXC を提供しているでしょう。

最初に LXC を使う場合は、LXC 4.0 の最新のバグフィックスのなされたバージョンのような、最新のサポート版リリースをお使いになることを推奨します。

Ubuntu を使っている場合、コンテナホストとして Ubuntu 18.04 LTS を使うことを推奨します。
LXC のバグフィックスリリースは、リリース後すぐに直接ディストリビューションのパッケージリポジトリ経由で利用可能で、パッチの当たっていないクリーンな最新版を提供します。

Ubuntu は、安全な非特権の LXC コンテナのために必要な全てをデフォルトで揃えている Linux ディストリビューションのいくつかのうちの 1 つです (Ubuntu 以外にもそのようなディストリビューションは存在します)。

Ubuntu では、LXC をインストールするのは次のように簡単です:

sudo apt-get install lxc

あなたのシステム上には、利用可能な LXC コマンドの全て、テンプレートの全て、LXC 処理のスクリプトに必要な python3 バインディングがインストールされるでしょう。

Linux カーネルに必要な機能を持っているかどうかをチェックするには次のコマンドを使います:

lxc-checkconfig

特権コンテナの作成

特権コンテナとは、root で作成し、root で実行するコンテナのことです。

特権コンテナは、LXC について学び、実験し始める場合のもっとも簡単な方法です。しかし、本番環境での使用に適していないかもしれません。ホストの Linux ディストリビューションによっては、特権コンテナは、一部のケーパビリティーの削除、AppArmor プロファイルや SELinux コンテキスト、Seccomp ポリシーといった機能で保護されている場合があります。しかし最終的には、プロセスは root 権限で実行されますので、特権コンテナ内の root へのアクセス権を信頼できない人に決して与えないでください。特権コンテナの安全性が低いとわかっても、それでも特権コンテナを作成しなければいけない場合、または特権コンテナがユースケースで特に必要な場合、特権コンテナを作成することはとても簡単です。デフォルトでは、LXC は特権コンテナを作成します。

以下の実行例で表示するターミナルのプロンプトは、実際に使用するコンピューターで表示されるプロンプトと違うかもしれません。ここで使用するターミナルプロンプトは、現在ホストのシェルにいるのか、コンテナのシェルにいるのか、そしてどのユーザーなのかを強調するためのものです。

特権コンテナは、次のコマンドで作成します。コンテナ名は、覚えやすい名前をつけられます。LXC のダウンロードテンプレートは、https://images.linuxcontainers.org/ で使えるイメージを選択するのに役立つでしょう。

root@host:~# lxc-create --name mycontainer --template download

使いたいコンテナイメージ名がわかっている場合は、ダウンロードテンプレートにオプションを指定できます。例えば、次のように指定します。

root@host:~# lxc-create --name mycontainer --template download -- --dist alpine --release 3.19 --arch amd64

コンテナが作成されたら、次のように起動できます。

root@host:~# lxc-start --name mycontainer

コンテナの状態の情報を見ることができます。

root@host:~# lxc-info --name mycontainer
Name:           mycontainer
State:          RUNNING
PID:            3250
IP:             10.0.3.224
Link:           vethgmeH9z
 TX bytes:      1.51 KiB
 RX bytes:      2.15 KiB
 Total bytes:   3.66 KiB

すべてのコンテナの状態の情報を見ることができます。

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 0         -      10.0.3.224 -    false

コンテナ内のシェルを起動します。

root@host:~# lxc-attach --name mycontainer

コンテナ内部では、システムコンテナとは何なのか、それが様々な点でどのように軽量な仮想マシンであるのかについて、実際に感じることができます。コンテナ内で行った変更は保存されます。あとでコンテナを停止して、再起動しても、変更はそのまま残っているでしょう。

コンテナを探索してみましょう。

root@mycontainer:~# cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.19.0
PRETTY_NAME="Alpine Linux v3.19"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

パッケージインデックスを更新し、インストールされたパッケージを更新し、さらに利用できるようにしたいほかのパッケージをインストールしてみます。

root@mycontainer:~# apk update

root@mycontainer:~# apk add --upgrade apk-tools

root@mycontainer:~# apk upgrade --available

root@mycontainer:~# apk add vim python3

コンテナのシェルから exit します。

root@mycontainer:~# exit

コンテナを停止します。

root@host:~# lxc-stop --name mycontainer

コンテナが二度と必要ではない場合、永久に削除できます。

root@host:~# lxc-destroy --name mycontainer

自動起動

デフォルトでは、コンテナはホストの再起動時には自動的に起動しません。コンテナ内で常に起動して実行されている必要がある Web アプリのようなサービスを運用しているケースがあるかもしれません。ホストの起動時にコンテナを起動するようにします。

先の説明のように、mycontainer という名前のコンテナの作成が済んでおり、起動しているとします。

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 0         -      10.0.3.30  -    false

コンテナの構成に設定行を追加することで、コンテナを自動起動するように設定できます。

root@host:~# echo "lxc.start.auto = 1" >>/var/lib/lxc/mycontainer/config

コンテナを設定したあと、ホストをリブートして、コンテナが実際に自動起動するかテストできます。

root@host:~# reboot

ホストを再起動して、ホストのシェルにログインできるようになったあと、コンテナが実行中で、コンテナの autostart プロパティが 1 に設定されていることがわかります。

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 1         -      10.0.3.30  -    false

It works!

作成するコンテナのいくつかで自動起動を設定したい場合、lxc-create で使う新しい設定ファイルを作成することをおすすめします。

root@host:~# cp /etc/lxc/default.conf /etc/lxc/autostart.conf

root@host:~# echo "lxc.start.auto = 1" >>/etc/lxc/autostart.conf

root@host:~# lxc-create --name containera --config /etc/lxc/autostart.conf --template download -- --dist alpine --release 3.19 --arch amd64

さらに別のオプションとして、すべてのコンテナで自動起動させたい場合、デフォルトの LXC 設定を変更できます。

安全のために、オリジナルの LXC の default.conf ファイルのバックアップを作成します。

root@host:~# cp /etc/lxc/default.conf /etc/lxc/default.conf.original

そして、デフォルトの設定を書き換えましょう。

root@host:~# echo "lxc.start.auto = 1" >>/etc/lxc/default.conf

デフォルト設定ファイルを使う、作成するすべてのコンテナが自動起動するようになります。例えば、

 root@host:~# lxc-create --name containerb --template download -- --dist alpine --release 3.19 --arch amd64

IP アドレス

上記の、lxc-info --name mycontainerlxc-ls --fancy の出力は、mycontainer がホストのローカルネットワーク上に IP アドレスを持っていることを示しています。

コンテナを起動した直後に lxc-ls の出力を確認すると、コンテナはまだ IP アドレスを持っていないことがわかるでしょう。

root@host:~# lxc-stop --name mycontainer

root@host:~# lxc-start --name mycontainer && lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
mycontainer RUNNING 1         -      -    -    false

5 秒ほど待って、再度確認すると、IP アドレスが設定されています。

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED
mycontainer RUNNING 1         -      10.0.3.152 -    false

コンテナが IP アドレスを持っていない場合、ファイアウォールの設定日本語訳)が必要かもしれません。例えば、Ubuntu 22.04 では次のようになります。

root@host:~# ufw allow in on lxcbr0
root@host:~# ufw route allow in on lxcbr0
root@host:~# ufw route allow out on lxcbr0

ここで lxcbr0/etc/default/lxc-net ファイル内の LXC_BRIDGE で設定されている値です。

インターネットへのアクセスが必要なコンテナ内で何か処理を行いたい場合、コンテナに IP アドレスが割り当たるまで待つ必要があります。lxc-info の出力に IP アドレスが含まれるまでポーリングするというのも、ひとつの方法でしょう。

root@host:~# lxc-start --name mycontainer

root@host:~# while ! lxc-info -n mycontainer | grep -Eq "^IP:\s*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\s*$"; do sleep 1; done; echo "Container connected!"
Container connected!

IP アドレス 10.0.3.152 は、前に見たときの 10.0.3.30 と同じではないことに注意してください。これは、コンテナがネットワークに参加する際に、IP アドレスが、ホストによってコンテナに動的に割り当てられるためです。

次のような現在のアドレスのリースのリストを確認できます。

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases 
1705896165 8e:e0:fc:72:79:65 10.0.3.152 mycontainer 01:8e:e0:fc:72:79:65

コンテナを停止すると、リースが削除されます。

root@host:~# lxc-stop --name mycontainer;

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases

コンテナを再起動すると、異なる IP アドレスの新しいリースが作成される可能性があります。

root@host:~# lxc-start --name mycontainer

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases
1705896699 26:61:b7:e3:53:73 10.0.3.110 mycontainer 01:26:61:b7:e3:53:73

これは真似しないでください。コンテナを強制的に削除しても、コンテナのリースはクリアされません。常に削除の前にコンテナを停止してください。

root@host:~# lxc-destroy --force --name mycontainer

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases
1705896699 26:61:b7:e3:53:73 10.0.3.110 mycontainer 01:26:61:b7:e3:53:73

DHCP 予約

コンテナに予測可能な IP アドレスが必要になる場合があるでしょう。ホスト上で DHCP 予約を行うと、コンテナがローカルネットワークに参加するたびに、コンテナに同じ IP アドレスが割り当たります。

DHCP 予約を有効にするには、/etc/default/lxc-netLXC_DHCP_CONFILE のコメントを外します。

root@host:~# sed -i 's|^#LXC_DHCP_CONFILE=.*$|LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf|' /etc/default/lxc-net

DHCP 予約を追加します。

root@host:~# echo "dhcp-host=mycontainer,10.0.3.100" >>/etc/lxc/dnsmasq.conf

IP アドレス(つまり、上記コマンドの 10.0.3.100)は、LXC_DHCP_RANGE 内のアドレスである必要があります。LXC_DHCP_RANGE を確認するには /etc/lxc/dnsmasq.conf ファイルを確認します。LXC_DHCP_RANGE="10.0.1.2,10.0.1.254" となっている場合は、前述のコマンドは 10.0.3.100 の代わりに、次のようになります。

root@host:~# echo "dhcp-host=mycontainer,10.0.1.100" >>/etc/lxc/dnsmasq.conf

また、IP アドレスが使用中であってはなりません。使用可能な IP アドレスを選択する方法の 1 つとして、上記セクションの作業中に動的に割り当てられたアドレスの 1 つ使う方法があります。

DHCP 予約を有効にしたら、lxc-net サービスを再起動します。

root@host:~# service lxc-net restart

コンテナを再起動します(途中でコンテナを削除した場合は再作成する必要があります)。

root@host:~# lxc-stop --name mycontainer

root@host:~# lxc-start --name mycontainer

数秒待って、コンテナの IP アドレスをチェックしましょう。

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 1         -      10.0.3.100 -    false

やったー! これで、コンテナが常に同じ IP アドレスを持つことに依存できます。

マウントするボリュームの追加

コンテナのファイルシステムの活動は /var/lib/lxc/<container-name>/rootfs に限られます。コンテナが削除されると、/var/lib/lxc/<container-name> のすべても削除されます。複数のコンテナがあり、それらの間でファイルシステムの一部を共有したい場合があるかもしれません。使い捨てのコンテナがあり、コンテナがなくなっても存続できるファイルシステム領域が必要な場合もあります。このような場合、コンテナの rootfs と別の場所にホストボリュームを作成し、コンテナ内にそのボリュームをマウントできます。

これまでのように、mycontainer という名前のコンテナを作成し、起動しているとします。

ホストボリュームを作成します。

root@host:~# mkdir -p /host/path/to/volume

コンテナ内でこのボリュームをマウントするために、2 つの方法があります。

最初のオプションには 2 ステップが必要です: コンテナ内にマウント先を手動で作成します。そしてコンテナへのマウントの設定を行います。

root@host:~# lxc-attach --name mycontainer -- mkdir -p /container/mount/point

root@host:~# echo "lxc.mount.entry = /host/path/to/volume container/mount/point none bind 0 0" >>/var/lib/lxc/mycontainer/config

2 つ目のオプションで必要な手順は 1 つだけです: マウントを設定するときに create=dir を使用します。これで、マウント先が自動的にコンテナ内に作成されます。

root@host:~# echo "lxc.mount.entry = /host/path/to/volume container/mount/point none bind,create=dir 0 0" >>/var/lib/lxc/mycontainer/config

どちらの方法でも、コンテナのマウント先のパス container/mount/point は相対パスであることに注意してください。先頭に / はありません。

コンテナを設定したら、新しい設定を使うように再起動します。

root@host:~# lxc-stop --name mycontainer

root@host:~# lxc-start --name mycontainer

ボリュームを作成して、コンテナ内にマウントしたので、それが機能しているかテストできます。

ホスト上で、ボリュームにテキストファイルを追加します。

root@host:~# echo "host message" >/host/path/to/volume/messages.txt

コンテナでシェルを起動します。

root@host:~# lxc-attach --name mycontainer

コンテナでテキストファイルの存在を確認し、その中身が見れます。

root@mycontainer:~# cat /container/mount/point/messages.txt
host message

コンテナからテキストにテキストを追加できます。

root@mycontainer:~# echo "mycontainer message" >>/container/mount/point/messages.txt

コンテナのシェルを exit します。

root@mycontainer:~# exit

ホストから、コンテナ内で追加したメッセージを見れます。

root@host:~# cat /host/path/to/volume/messages.txt 
host message
mycontainer message

root ユーザーによる共有の UID と GID レンジを持つ非特権コンテナの作成

システム全体の非特権コンテナ(rootが作成して起動する非特権コンテナ)を作成するには、サブ UID とサブ GID を構成するためのステップがいくつか必要です。

具体的には、サブ UID と GID の範囲を、/etc/subuid/etc/subgid で root に手動で割り当てます。そして、/etc/lxd/default.conf 内に lxc.idmap を使って、それらの範囲を設定する必要があります。

たとえば、ホスト上でサブ UID と GID の範囲に関して何も設定していない場合、必要なのは次のようなコマンドだけです。ここで示した操作を行う前に、/etc/subuid/etc/subgid を調べて、ホスト上で 100000:65536 の範囲がまだ使われていないことを確認してください。範囲が使用中の場合、他の範囲を使えます。

echo "root:100000:65536" >>/etc/subuid
echo "root:100000:65536" >>/etc/subgid
echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/default.conf
echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/default.conf

これで終わりです。root ユーザーで作成するコンテナはすべて、特権なしの実行になるでしょう。例えば、

lxc-create --name container1 --template download
lxc-create --name container2 --template download

/etc/lxc/default.conf 内の変更されたデフォルト設定を使って作成されたコンテナはすべて、同じサブ UID と GID の範囲を共有することに注意してください。これは、コンテナがそれぞれ独立したサブ UID と GID を使うよりはセキュアではない可能性があります。

コンテナを起動すると、コンテナ側から見た UID の範囲と比較することで、ホスト側から見た UID の範囲を見ることができます。

lxc-start --name container1
ps aux
lxc-attach --name container1 -- ps aux

root ユーザーによる独立した UID と GID の範囲を持つ非特権コンテナの作成

コンテナごとに独立したサブ UID と GID の範囲を使うことで、あるコンテナでのセキュリティ侵害で他のコンテナにアクセスできなくなります。

2 つのコンテナを作りたいとすると、次のようにします(/etc/lxc/default.conf は前のセクションのように変更されていない前提です)。

独自の UID と GID の範囲を持つ最初のコンテナを設定し、作成します。

echo "root:100000:65536" >>/etc/subuid
echo "root:100000:65536" >>/etc/subgid
cp /etc/lxc/default.conf /etc/lxc/container1.conf
echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/container1.conf
echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/container1.conf
lxc-create --config /etc/lxc/container1.conf --name container1 --template download

異なる UID と GID の範囲を使って、2 つめのコンテナを設定し、作成します。

echo "root:200000:65536" >>/etc/subuid
echo "root:200000:65536" >>/etc/subgid
cp /etc/lxc/default.conf /etc/lxc/container2.conf
echo "lxc.idmap = u 0 200000 65536" >>/etc/lxc/container2.conf
echo "lxc.idmap = g 0 200000 65536" >>/etc/lxc/container2.conf
lxc-create --config /etc/lxc/container2.conf --name container2 --template download

コンテナの作成後、/etc/lxc/container1.conf/etc/lxc/container2.conf は削除しても構いません。

一般ユーザーによる非特権コンテナの作成

非特権コンテナは最も安全なコンテナです。
非特権コンテナでは、コンテナで使う範囲の uid と gid を割り当てるために、uid と gid のマッピングを使います。
これはコンテナ内の uid 0 (root) が、コンテナの外では実際は uid が 100000 を持つというようになります。
それゆえ、万が一間違って問題のある操作を行ったり、攻撃者がコンテナを抜けだそうとしても、nobody ユーザと同じ程度の権限しか自身にないことがわかるでしょう。

残念ながら、このことは同時に以下のような操作が非特権コンテナでは許可されないことを意味します:

  • ほとんどのファイルシステムのマウント
  • デバイスノードの作成
  • マッピングが存在していない uid/gid に対する操作

このため、ほとんどのディストリビューションのコンテナテンプレートは動作しないでしょう。
代わりに、このような非特権の環境でも動くことを確認した、あらかじめビルド済みのディストリビューションのイメージを提供する "download" テンプレートを使う必要があります。

このあとの説明は、最新のカーネル、最新バージョンの shadow、libpam-cgfs、デフォルトの uid/gid 割り当てと言った、最新の Ubuntu や同等の Linux ディストリビューションを使用していると仮定して行います。

まず第一に、お使いの (非特権コンテナを使おうとする) ユーザが /etc/subuid/etc/subgid で定義された uid/gid のマッピングを持っている必要があります。Ubuntu では、デフォルトで 65536 個の uid と gid の割り当てが、システム上で全ての新規ユーザに与えられますので、Ubuntu をお使いの場合はすでにそのマッピングを持っているはずです。もしマッピングがない場合は、usermod コマンドを使って割り当てる必要があります。

次に、非特権ユーザに与えるネットワークデバイスの範囲を設定するために使う /etc/lxc/lxc-usernet を設定します。
デフォルトでは、ホスト上で全くネットワークデバイスを割り当てできないことになっていますので、このファイルに以下のような設定を追加します:

echo "$(id -un) veth lxcbr0 10" | sudo tee -a /etc/lxc/lxc-usernet

これは、"your-username" にブリッジ lxcbr0 に接続する 10 個の veth デバイスの作成を許可するという意味です。

これが済むと、最後のステップは LXC の設定ファイルの作成です。

  • ~/.config/lxc ディレクトリがない場合は作成します。
  • /etc/lxc/default.conf~/.config/lxc/default.conf にコピーします。
  • 以下の 2 行をコピーしたファイルに追加します。
    • lxc.idmap = u 0 100000 65536
    • lxc.idmap = g 0 100000 65536

ここで設定した値は /etc/subuid/etc/subgid にある値と一致している必要があり、標準の Ubuntu システムの初期ユーザのために存在が必要です。

mkdir -p ~/.config/lxc
cp /etc/lxc/default.conf ~/.config/lxc/default.conf
MS_UID="$(grep "$(id -un)" /etc/subuid  | cut -d : -f 2)"
ME_UID="$(grep "$(id -un)" /etc/subuid  | cut -d : -f 3)"
MS_GID="$(grep "$(id -un)" /etc/subgid  | cut -d : -f 2)"
ME_GID="$(grep "$(id -un)" /etc/subgid  | cut -d : -f 3)"
echo "lxc.idmap = u 0 $MS_UID $ME_UID" >> ~/.config/lxc/default.conf
echo "lxc.idmap = g 0 $MS_GID $ME_GID" >> ~/.config/lxc/default.conf

現時点の Ubuntu LTS 20.04 は次の追加の手順が必要です:

export DOWNLOAD_KEYSERVER="hkp://keyserver.ubuntu.com"

そして、最初のコンテナを作成しましょう:

systemd-run --unit=my-unit --user --scope -p "Delegate=yes" -- lxc-create --name my-container --template download

ダウンロードするテンプレートでは、選択できるディストリビューション、バージョン、アーキテクチャが表示されます。例えば、"ubuntu"、"focal"(20.04 LTS)、"amd64" のようなものです。別の良い例は、"alpine"、"3.19"、"amd64" です。

非特権ユーザーとして非特権コンテナを実行するには、事前に空の権限移譲された cgroup を割り当てる必要があります(これが必要な理由は cgroup2 のリーフノードと権限移譲モデルのためであり、liblxc で必要なわけではありません)。より詳細な情報は cgroups: cgroup2 のフルサポートをご覧ください。

ユーザーとしてシェルからコンテナを単純に起動し、自動的に cgroup を権限移譲することはできません。そのため、lxc-* コマンド群を呼び出すごとに、systemd-run コマンドでラップする必要があります。例えば、コンテナを起動するには、単に lxc-start mycontainer と実行する代わりに次のように実行します:

systemd-run --unit=my-unit --user --scope -p "Delegate=yes" -- lxc-start --name mycontainer

注意: もし、LXC をインストールする前に libpam-cgfs がホストマシン上にインストールされていない場合、最初のコンテナを作成する前にそのユーザが正しい cgroup に確実に所属しているようにする必要があります。これはログアウト・ログインするか、ホストマシンをリブートするとそのようになるでしょう。

実行したコンテナのステータスは以下のどちらかで確認できます:

lxc-info --name mycontainer
lxc-ls --fancy

コンテナ内でシェルを実行するには以下のようにします:

lxc-attach --name mycontainer

コンテナの停止は以下のように行います:

lxc-stop --name my-container

最後にコンテナを消去するには以下のようにします:

lxc-destroy --name my-container

各ディストリビューションの LXC に関するドキュメント