Linuxコマンドを叩いて、コンテナを自作

2023年12月12日掲載

governance

この記事では、Linux上の機能を組み合わせて、一からコンテナを作りあげます

コンテナをコマンドで操作する、ではありません。昨今のDocker事情にも触れません。Kubernetesのようなオーケストレーションツールの話もしません。AWS、Azure、Google Cloud、IBM Cloudなどのクラウドの話題も出てきません。

システムコールがカーネルに届くことを感じながら、コンテナを自作します

以下が完成図です。本記事の読了後には、この構成も理解できるようになります。

目次

この記事では
  • サーバ上で、コマンドを叩いたり、プログラム開発をすることは慣れているけれど、Dockerなどのアプリケーションコンテナの話題になると少し不安を覚える方向けの記事です。
  • コンテナイメージの管理ファイルを作り、Dockerコマンドを打ちデプロイはできるけれど、サーバサイドのカーネルレイヤーと断絶されている抽象性を感じ、何がどうなっているか理解に自信がない方を対象としています。

名前空間についての基礎知識

名前空間とは何かを、まずは理解します。

サーバへログインしてみてください。何かしらのシェル上にいると思いますが、このプロセスは既にデフォルトの名前空間に属しています。名前空間とは、プロセスと実行の論理的な境界です。ネームスペース(namespace: ns)とも言います。

実際にシェルの名前空間を確かめてみましょう。

/proc/[pid]/nsサブディレクトリは、擬似的なファイルシステムであり、カーネル内のデータへのインターフェースとして機能しています。これにより、各プロセス(プロセスIDを指定したpid)に関連するネームスペース情報が提供されます。

# カレントシェル($$)のPIDを確認
$ echo $$
132860

### 上記のpidがシェルであることも念のため確認
$ ps | grep $$ | grep -v grep
132869 pts/2    00:00:00 bash

システムの状態などをファイルとして記録しているprocディレクトリから、自身のプロセスが所属するnsの一覧をのぞいてみます。

$ ls -l /proc/$$/ns
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 net -> 'net:[4026531840]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 time -> 'time:[4026531834]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 user -> 'user:[4026532250]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:49 uts -> 'uts:[4026531838]'

cgoup、ipc、mnt・・・と、何かしらの役割ごとに名前空間があり、それぞれに数字のidが振られています。
bashプロセスは、cgroupの4026531835というidのnsに、ipcの4026531839のnsに属しています。

子プロセスも親と同じ名前空間に属します。適当なプロセスを稼働させてみます(バックグラウンドのプロセスは$!で取得できます)。

$ sleep 60 &

$ ls -l /proc/$!/ns
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 net -> 'net:[4026531840]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 time -> 'time:[4026531834]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 user -> 'user:[4026532250]'
lrwxrwxrwx 1 nobody nobody 0 Apr 28 13:52 uts -> 'uts:[4026531838]'

それぞれの名前空間が示すidは親プロセスと同じですね。

コンテナの正体

ここまでの知識だけで、コンテナとは何かを説明できます。

サーバシステム上のさまざまなリソースや役割に名前を決め、それらをプロセス単位に、見せたり、使わせたりするよう、論理的に名前空間として区切った分離単位がコンテナです。グローバルなリソースを、あるプロセスのみに隔離する、これがコンテナの正体です。

カーネルにはコンテナという機能はありません。

先の例では、cgroup、ipc、mnt、net、pid、pid_for_children、time、time_for_children、user、utsという役割の名前空間が利用されていました。

どの役割の名前空間を利用すればコンテナになるかは決まっていません。分離したい目的に合致していればよいです。ある一つだけの役割の名前空間分離でも、定義上はコンテナです。

コンテナを作る

サーバにログインした時点で、既にコンテナを利用していることが分かりました。名前空間による分離が肝です。

この知識があれば、コンテナを作ることもできそうですね。プロセスの実行コンテキストを、名前空間で分離するために/procディレクトリを操作すればよいです。ただし、/procディレクトリを直接触るような危険な手順は不要です。コマンドが用意されています。unshareコマンドを打てば終わりです。

それでは、馴染みのありそうな役割の名前空間をピックアップして、コンテナ作りをやっていきましょう。

unshareコマンドのオプションは目的にあわせて変えています。詳細はmanをご確認ください。

 

mnt名前空間
mountの略です。文字通り、マウント用の名前空間です。

unshareコマンドの前後に、確認用のコマンドを入れています。本質的な目的とは関係ありません。名前空間が分かれていることを視認するためです。

# 現在のmntの名前空間
$ readlink /proc/$$/ns/mnt
mnt:[4026531841]


# 新規にmntの名前空間を作成
$ sudo unshare --mount --propagation=private /bin/bash

# idが変わったことを確認
# readlink /proc/$$/ns/mnt
mnt:[4026532251]

# idだけではなく、実マウントポイントが分離されるかを実験
# mkdir tmpdir
# mount -t tmpfs tmpmnt tmpdir

# findmnt tmpmnt
TARGET          SOURCE FSTYPE OPTIONS
/var/tmp/tmpdir tmpmnt tmpfs  rw,relatime,inode64

# exit

# exitしたあとは、名前空間が異なるため、見えないで正解です
$ find tmpmnt
find: 'tmpmnt': No such file or directory

unshareコマンドのpropagationというオプションにprivateを与えない場合、マウントポイントを共有して見せたりもできるのですが、詳細は割愛します。

 

uts名前空間
UNIX Time-Sharingの略で、ホスト名やドメイン名を分離をします。この場合もunshareコマンドを打つだけです。
名前空間ごとにホスト名が分離されることを確かめるための、検証目的のコマンドを挟んでいます。

$ readlink /proc/$$/ns/uts
uts:[4026531838]

$ hostname
mylinux

$ sudo unshare --uts /bin/bash

# readlink /proc/$$/ns/uts
uts:[4026532250]

# hostname mylinux2

# exit

$ hostname
mylinux

 

pid名前空間
プロセスの名前空間を分離します。
dockerのRUNで起動したプロセスが pid=1 になっているのを見かけたことがあるかと思いますが、pid名前空間によるものです。最初に作られたbashプロセスに、pidの1が付与されます。

$ ps aux
いろいろ

$ readlink /proc/$$/ns/pid
pid:[4026531836]

$ sudo unshare --pid --mount-proc --fork /bin/bash

# ps
    PID TTY          TIME CMD
      1 pts/0    00:00:00 bash
     23 pts/0    00:00:00 ps

 

net名前空間

ネットワーク周りも確認しておきましょう。

$ readlink /proc/$$/ns/net
net:[4026531840]

$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
-snip-

$ sudo unshare --net /bin/bash

# readlink /proc/$$/ns/uts
uts:[4026531838]

$ ifconfig
出力なし( ネットワークを構成することも可能です)

他にもまだまだ分離する役割はありますが、この辺りにしておきましょう。

まとめ

現代のITサービスにおいて、コンテナ技術は重要な役割を果たしています。そのコンテナの正体を解明し、仕組みを理解するために、手動で実際にコンテナを作る体験をやってみました。

今後もますますコンテナの利用範囲と、期待される役割は増えていくでしょう。さまざまなツールやオーケストレーションツールも日々登場しています。ついていくのだけでも大変、と慌ててしまいそうになりますが、それらの土台となる技術が何であり、本質的にコンテナが何を目的としているのか理解できていると、新しく出てくるコンテナ技術へも慌てず追随できるのではないでしょうか。

おすすめの記事

条件に該当するページがございません