ようへいの日々精進XP

よかろうもん

俺は Linux コンテナについてなんにも解っていなかった 〜 haconiwa で学ぶ Linux コンテナ (1) 〜

追記

@udzura さんからコメントを頂きました!

有難うございました!

直近の Docker 界隈について

以下のようなキーワードがインターネット上を駆け巡っている気がする。

自分自身、これのらのキーワードについて中身を全く理解出来ていない。

また、Docker を触る機会も減っているのも事実で、やばいな〜、やばいな〜と思いながら、ゴールデンウィーク突入となったので、改めて、Docker というよりも Linux コンテナ技術について勉強してみたいと思う。

Linux コンテナ

参考

speakerdeck.com

Linux コンテナとは

Linux カーネルの機能で以下のような機能を提供する。

  • 隔離された空間でプロセスを実行する
  • プロセスに対してリソース制限を設定する

Linux コンテナを構成する主な機能

Linux コンテナはカーネルに含まれる以下のような機能を利用して実装されている。

名前 役割
namespace OS リソースの隔離(プロセスをグループ化して他のリソースと隔離)
cgroup ホストの物理リソースに対する制限(グループ化したプロセス対するリソース制限)
capability root 権限をプロセスやファイルに割り当てる

上記以外にも Bind mount/chroot や Resource limit(rlimit) 及び setuid/setgid 等の機能も利用されている。

OS リソース毎の Namespace

OS リソース毎に以下のような Namespace が提供されている。

  • Mount Namespace
  • UTS Namespace
  • PID Namespace
  • IPC Namespace
  • User Namespace
  • Network Namespace
  • cgroup Namespace

各 Namespace はカーネルのバージョンを追う毎に追加実装されている。

cgroup サブシステム

cgroup は cgroup ファイルシステムという仮想的なファイルシステムを使って操作し、Namespace 同様に以下のようなサブシステムと呼ばれる機能でリソースを扱う。

  • cpu
  • cpuacct
  • cpuset
  • device
  • freezer
  • memory
  • blkio

他にも hugetlb や perf_event 等が提供されている。

こんなにざっくりでは、Linux コンテナは語れないと思うけど

最低限、上記のようなキーワードだけはしっかりと覚えておきたい。

haconiwa

haconiwa とは

github.com

  • @udzura さんがメインとなって実装されている mruby で実装された Linux コンテナのランタイム
  • Ruby DSL で Linux コンテナの設定を記述することが出来て、自分だけの Linux コンテナを作成することが出来る

haconiwa で何が出来ると?

  • 利用するコンテナ要素を自由に選択出来る
  • Ruby DSL を使ってプログラマブルに Linux コンテナを作成出来る
  • プログラマブルになることで動的なりソース割り当てが出来る等の柔軟な運用が可能

haconiwa å°Žå…¥

haconiwa は mruby-cli でビルドされたバイナリで配布されており、packagecloud にてパッケージ配布されているので、以下のようにインストールする。

#
# Ubunt/xenial に導入する
#
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"

#
# 事前に lxc を導入済み
#
$ dpkg --list | grep lxc
ii  liblxc1                          2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (library)
ii  lxc                              2.0.7-0ubuntu1~16.04.2                     all          Transitional package for lxc1
ii  lxc-common                       2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (common tools)
ii  lxc-templates                    2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (templates)
ii  lxc1                             2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools
ii  lxcfs                            2.0.6-0ubuntu1~16.04.1                     amd64        FUSE based filesystem for LXC
ii  python3-lxc                      2.0.7-0ubuntu1~16.04.2                     amd64        Linux Containers userspace tools (Python 3.x bindings)

#
# haconiwa パッケージの導入
#
$ curl -s https://packagecloud.io/install/repositories/udzura/haconiwa/script.deb.sh | sudo bash
$ sudo apt-get install haconiwa=0.8.5-1

#
# ヘルプの確認
#
ubuntu@ubuntu-xenial:~$ haconiwa
haconiwa - The MRuby on Container
commands:
    new       - generate haconiwa's config DSL file template
    create    - create the container rootfs
    provision - provision already booted container rootfs
    archive   - create, provision, then archive rootfs to image
    start     - run the container
    attach    - attach to existing container
    reload    - reload running container parameters, following its current config
    kill      - kill the running container
    version   - show version
    revisions - show mgem/mruby revisions which haconiwa bin uses

Invoke `haconiwa COMMAND -h' for details.

はじめての haconiwa (1)

以下のように haconiwa new を実行すると定義ファイルの雛形を生成する事が出来る。

haconiwa new \
--name=my-first-container \
--root=/var/lib/haconiwa/my-first-container my-first-container.haco

以下のように出力され、my-first-container.haco というファイルがカレントディレクトリに生成されている。

create  my-first-container.haco

.haco ファイルの中身は以下のようになっている。

$ grep -v -e '^\s*#' -e '^\s*$' my-first-container.haco
Haconiwa.define do |config|
  config.name = "my-first-container"
  config.init_command = "/bin/bash"
  root = Pathname.new("/var/lib/haconiwa/my-first-container")
  config.chroot_to root
  config.bootstrap do |b|
    b.strategy = "lxc"
    b.os_type  = "alpine"
  end
  config.provision do |p|
    p.run_shell <<-SHELL
apk add --update bash
    SHELL
  end
  config.add_mount_point "tmpfs", to: root.join("tmp"), fs: "tmpfs"
  config.mount_network_etc(root, host_root: "/etc")
  config.mount_independent "procfs"
  config.mount_independent "sysfs"
  config.mount_independent "devtmpfs"
  config.mount_independent "devpts"
  config.mount_independent "shm"
  config.namespace.unshare "mount"
  config.namespace.unshare "ipc"
  config.namespace.unshare "uts"
  config.namespace.unshare "pid"
end

はじめての haconiwa (2) 〜 コンテナ作成 〜

以下のように create オプションを指定して haconiwa を実行すると、lxc-create が実行されて alpine linux のコンテナの作成が開始される。

sudo haconiwa create my-first-container.haco

以下のように出力される。

ubuntu@ubuntu-xenial:~$ sudo haconiwa create my-first-container.haco
Creating rootfs of my-first-container...
Start bootstrapping rootfs with lxc-create...
[bootstrap.lxc]:        Obtaining an exclusive lock... done

...

Command success: lxc-create -n my-first-container -t alpine --dir /var/lib/haconiwa/my-first-container exited 0
Success!
Start provisioning...
Running provisioning with shell script...
[provison.shell-1]:     + apk add --update bash
[provison.shell-1]:     fetch http://mirror.yandex.ru/mirrors/alpine//v3.5/main/x86_64/APKINDEX.tar.gz
[provison.shell-1]:     (1/5) Installing ncurses-terminfo-base (6.0-r7)
[provison.shell-1]:     (2/5) Installing ncurses-terminfo (6.0-r7)
[provison.shell-1]:     (3/5) Installing ncurses-libs (6.0-r7)
[provison.shell-1]:     (4/5) Installing readline (6.3.008-r4)
[provison.shell-1]:     (5/5) Installing bash (4.3.46-r5)
[provison.shell-1]:     Executing bash-4.3.46-r5.post-install
[provison.shell-1]:     Executing busybox-1.25.1-r0.trigger
[provison.shell-1]:     OK: 14 MiB in 21 packages
Command success: /bin/sh -xe exited 0
Success!

はじめての haconiwa (3) 〜 コンテナ起動 〜

以下のように run 又は start オプションを指定して haconiwa を実行するとコンテナが起動する!!

$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=1893
bash-4.3# ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
    2 root       0:00 ps
bash-4.3# exit
exit
Container(1893) finish detected: #<Process::Status: pid=1893,exited(0)>
Container successfully exited: #<Process::Status: pid=1893,exited(0)>
One of supervisors finished: 1892, #<Process::Status: pid=1892,exited(0)>

$ sudo haconiwa start my-first-container.haco
Container fork success and going to wait: pid=1906
bash-4.3# ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
    2 root       0:00 ps
bash-4.3# exit
exit
Container(1906) finish detected: #<Process::Status: pid=1906,exited(0)>
Container successfully exited: #<Process::Status: pid=1906,exited(0)>
One of supervisors finished: 1905, #<Process::Status: pid=1905,exited(0)>

ちなみに、起動したコンテナからインターネットへのアクセスだって OK 牧場。

$ sudo haconiwa start my-first-container.haco
Container fork success and going to wait: pid=1919
bash-4.3#
bash-4.3# ping www.yahoo.com -c 3
PING www.yahoo.com (106.10.139.246): 56 data bytes
64 bytes from 106.10.139.246: seq=0 ttl=63 time=137.592 ms
64 bytes from 106.10.139.246: seq=1 ttl=63 time=158.620 ms
64 bytes from 106.10.139.246: seq=2 ttl=63 time=106.147 ms

--- www.yahoo.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 106.147/134.119/158.620 ms
bash-4.3#

haconiwa で学ぶ Linux コンテナ

.haco ファイル再掲

ここからは、以下の .haco ファイルを利用して Linux コンテナを弄ってみたいと思う。

Haconiwa.define do |config|
  config.name = "my-first-container"
  config.init_command = "/bin/bash"
  root = Pathname.new("/var/lib/haconiwa/my-first-container")
  config.chroot_to root
  config.bootstrap do |b|
    b.strategy = "lxc"
    b.os_type  = "alpine"
  end
  config.provision do |p|
    p.run_shell <<-SHELL
apk add --update bash
    SHELL
  end
  config.add_mount_point "tmpfs", to: root.join("tmp"), fs: "tmpfs"
  config.mount_network_etc(root, host_root: "/etc")
  config.mount_independent "procfs"
  config.mount_independent "sysfs"
  config.mount_independent "devtmpfs"
  config.mount_independent "devpts"
  config.mount_independent "shm"
  config.namespace.unshare "mount"
  config.namespace.unshare "ipc"
  config.namespace.unshare "uts"
  config.namespace.unshare "pid"
end

namespace を弄る前に

Namespace の機能についておさらい。

namespace 機能
mount Namespace 内の mount / umount が他の Namespace に影響を与えないようにする
ipc SysV IPC オブジェクトや POSIX メッセージキューの隔離
uts hostname や uname の結果を分離
pid PID 空間の分離、新しい PID Namespace では PID 1 から始まる

uts と pid について弄ってみる。

namespace uts

以下のように config.namespace.unshare "uts" をコメントアウト。

ubuntu@ubuntu-xenial:~$ diff -u my-first-container.haco.bk my-first-container.haco
--- my-first-container.haco.bk  2017-04-29 23:35:21.313501998 +0000
+++ my-first-container.haco     2017-04-29 23:35:32.883713999 +0000
@@ -59,7 +59,7 @@
   # The namespaces to unshare:
   config.namespace.unshare "mount"
   config.namespace.unshare "ipc"
-  config.namespace.unshare "uts"
+  # config.namespace.unshare "uts"
   config.namespace.unshare "pid"

   # You can use existing namespace via symlink file. e.g.:

何が起きるのか。

ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial
ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3449
bash-4.3# hostname
ubuntu-xenial

uts は hostname や uname の結果を隔離する namespace であるが、unshare を行っていない為、hostname の結果が隔離されておらず、ホストと同じ hostname の結果を返している状態。

ちなみに、この状態でコンテナ内からホスト名を変更してみると、以下のように怒られる。

bash-4.3# hostname foo
hostname: sethostname: Operation not permitted

これは、sethostname(2) の

EPERM
sethostname() において、呼び出した人が CAP_SYS_ADMIN ケーパビリティ (capability) を持っていなかった。

とある事から察するに、haconiwa においてコンテナ内部からのホスト名変更は Linux capability により制御されていると思われる。

改めて、config.namespace.unshare "uts" をコメントインして haconiwa run してみる。

ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial
ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3651
bash-4.3# hostname
my-first-container

hostname コマンドの実行結果がホスト側とコンテナ内で異なっており、コンテナに隔離された状態となっている。

尚、unshare コマンドで uts のみを分離した場合には以下のような挙動となり、隔離した環境において実施した hostname コマンドの影響は限定的となっていることが解る。

#
# uts namespace を隔離
#
ubuntu@ubuntu-xenial:~$ sudo unshare --uts -- /bin/bash
root@ubuntu-xenial:~# hostname
ubuntu-xenial
root@ubuntu-xenial:~# hostname foo
root@ubuntu-xenial:~# hostname
foo

#
# 別端末で hostname を確認(hostname 変更の影響は出ていない)
#
ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial
ubuntu@ubuntu-xenial:~$ hostname
ubuntu-xenial

namespace pid

以下のように config.namespace.unshare "pid" をコメントアウト。

$ diff -u my-first-container.haco.bk my-first-container.haco
--- my-first-container.haco.bk  2017-04-29 23:35:21.313501998 +0000
+++ my-first-container.haco     2017-04-29 23:56:19.213720735 +0000
@@ -60,7 +60,7 @@
   config.namespace.unshare "mount"
   config.namespace.unshare "ipc"
   config.namespace.unshare "uts"
-  config.namespace.unshare "pid"
+  # config.namespace.unshare "pid"

   # You can use existing namespace via symlink file. e.g.:
   # config.namespace.enter "net", via: "/var/run/netns/sample001"

何が起きるのか…

ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3596
bash-4.3# ps aux | less

PID   USER     TIME   COMMAND
    1 root       0:13 {systemd} /sbin/init
    2 root       0:00 [kthreadd]
    3 root       0:00 [ksoftirqd/0]

...

3596 root       0:00 /bin/bash
3602 root       0:00 [kworker/u4:2]
3605 root       0:00 ps aux
3606 root       0:00 less

ホスト側のプロセスが丸見え状態になっている。

改めて、config.namespace.unshare "pid" をコメントインして haconiwa run してみる。

ubuntu@ubuntu-xenial:~$ sudo haconiwa run my-first-container.haco
Container fork success and going to wait: pid=3623
bash-4.3# ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/bash
    2 root       0:00 ps aux

プロセス ID は 1 から開始され、ホスト側のプロセス ID を見ることは出来ない状態になっておりコンテナに隔離された状態となっている。

俺は Linux コンテナについてなんにも解っていなかった(まとめ)

haconiwa は

  • Ruby DSL でコンテナ定義を書くことが出来る Linux コンテナランタイム
  • 手軽に色々な状態のコンテナを作成することが出来るので Linux コンテナを学ぶのに打ってつけ

Linux コンテナ

  • 自分は本当になんにも解っていなかった
  • Linux コンテナとは namespace による OS リソースの隔離、cgroup によるマシンリソースの制限等の Linux カーネルの機能を寄せ集めて実装されている
  • Docker は良しなに Linux コンテナをラップして使いやすくしただけ(それが凄いことなんだけど)で、コンテナを動かすのに Docker は必ずしも必要無いんだなってことが解った

次回は

cgroup や capability に触れられれば良いかなと考えている。

参考

Linux コンテナ神資料

Linux コンテナを語る上で欠かせない神資料達。

haconiwa 神資料

haconiwa を触る上で欠かす事の出来ない神資料達。