大学生がする、Linuxの機能から自作コンテナを作ろう

# はじめに
コンテナ技術を学部の卒業論文のテーマにしたいと思い、調査をしていましたが厳しそうなのでOutputとして残すために投稿します。
また、本記事の対象者はDockerは使ったことあるけど中身を知らない、コンテナって美味しいの??という方向けです。簡単にコンテナがどのようにできているかを紹介した後、Linuxの機能を使ってコマンドラインベースで自作コンテナを立てます。
本記事投稿時は心がまっさらな純粋無垢な学生であるため、暖かいマサカリ頂けると幸いです。
ちなみに参考にさせていただいた記事も貼っておきます。[^1]
基本的に参考記事に記載してあることは深くは触りません。本記事の目的としてはコンテナの簡単な理解と参考記事になかった、作成したコンテナのネットワークの設定をすることです。


# コンテナとは??
まずは冒頭でも触れた通り簡単にコンテナの紹介をします。
コンテナとはただのプロセスです。

## アーキテクチャ
![VM vs Container](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632406/9c2e300d-f272-7966-0062-f6b1be08231a.png "VM vs Container")

上記の図の左がVMのアーキテクチャであり、右がコンテナのアーキテクチャです。[^2]
このようにコンテナはホストのカーネルを共有しつつ、コンテナランタイムと呼ばれるコンテナの実行環境の上で各アプリケーションが稼働しています。Dockerはそのコンテナの実行環境の一つです。
ではそのコンテナランタイムとは何か??

## コンテナランタイム
コンテナランタイムについてまとめてあるQiitaの記事を記載しておきます。
[コンテナランタイムの動向を整理してみた件](https://qiita.com/cfg17771855/items/0c0f5df26ffcfe18a74d)
記事を見ていただければお分かりになるかと思いますが主にHigh-LevelとLow-Levelに分かれている、ということですね。
また、CRIやOCIで標準化されているためそれに準拠したソフトウェアであれば互換性が保てるということが私個人的には素晴らしいと感じました。

## Low-Level
代表例としては`runc`や`runsc`、`runcc`などがあると思います。
役割としてはコンテナを隔離して制限する部分を担っています。
ソースコードを見ていないので断言はできませんが、Linux環境下ではこのLow-Levelは基本的にLinuxの機能を利用して動いているため、やろうと思えば自作コンテナが作れてしまうということです:fire::fire::fire:


# コンテナを自作するぞ!!!
コンテナがどのようなものか簡単に紹介しましたが、上記の説明では満足に理解できないと思うので各々調べていただけると幸いです。

## 環境
| 種類 | 環境 |
|:-|:-|
| OS | Ubuntu 20.04.2 LTS |
| Host | macOS 11.4 |
| Host Network | 192.168.10.1/24 |
| Gest Network to Host | 192.168.10.10/24 |
| br0(Gest Bridge to Container) | 10.10.0.1/24 |
| Container Network | 10.10.0.10/24 |

```bash
lesson@lesson-VirtualBox:~$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04

MacBook:~ user$ sw_vers
ProductName:	macOS
ProductVersion:	11.4
```
GestOS内のネットワークは下記のようにします。
![GestOS Network](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632406/ac85c559-6be3-c328-f156-5909eb030e0a.png)

ネットワークについては本記事では説明しないため、わからない方はぜひ調べてください。

## 要素技術
Linuxの機能を使っていくため、その要素技術を記載していきます。
参考記事にその説明があるため深くは記述しませんが、本記事はその先のネットワークの設定まで行っています。

### unshare
コンテナの分離を実現する重要なNamespaceを隔離するコマンドです。
unshareにはオプションが用意されており、それぞれの説明は参考記事をご覧ください。[^1]
今回は下記のコマンドを利用します。

```bash
 unshare -muinpfr /bin/sh -c
```

### cgcreate, cgset, cgexec
コンテナの分離を実現する重要なCGroupを設定するコマンドです。
cgroupfsでも設定することができますが、コマンドベースで行いため今回は上記3つのコマンドを利用してCGroupを設定していきます。
filesystemで理解したい方はこちらの記事を参照してください。
[LXCで学ぶコンテナ入門 －軽量仮想化環境を実現する技術 第3回　Linuxカーネルのコンテナ機能［2］ ─cgroupとは？（その1）](https://gihyo.jp/admin/serial/01/linux_containers/0003)
今回は下記のコマンドを利用します。

```bash
UUID=$(uuidgen)
sudo cgcreate -t $(id -un):$(id -gn) -a $(id -un):$(id -gn) -g cpu,memory:$UUID
cgset -r memory.limit_in_bytes=10000000 $UUID
cgset -r cpu.cfs_period_us=1000000 $UUID
cgset -r cpu.cfs_quota_us=300000 $UUID

cgexec -g cpu,memory:$UUID
```
`uuidgen`でUUIDを作成し、その名前でサブグループを作成、それぞれ制限をつけて、実行する、という流れです。
こちらも参考記事に詳細がありますので参照してください。


### capsh
コンテナの制限を実現する重要なCapabilitiesで実行するコマンドです。
Capability設定をサポートした `/bin/bash` のラッパーです。
今回は下記のコマンドを利用します。

```bash
exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'
```

### ip
ip netnsはunshareと同様にNamespaceを隔離するコマンドですが、Network Namespaceを隔離、設定する際に利用します。
今回はunshareですでにNetwork Namespaceを作成していますのでそのNamespaceにネットワークを設定する際に利用します。


## いざ実践、、!!
簡単にコンテナの紹介とコマンドの紹介をしたところで実際にコンテナを作ってみましょう。
先に、コマンドラインベースで行い、ネットワークを確認するため2つ以上のターミナルからGestOSに対してsshしておきましょう。

```bash
## ターミナル1
lesson@lesson-VirtualBox:~$ tty
/dev/pts/0

## ターミナル2
lesson@lesson-VirtualBox:~$ tty
/dev/pts/1
```

ではまず、ターミナル1でtempディレクトリを作成し、Dockerのfilesystemをtempディレクトリ上に展開し、bashが動くようにリンクを貼ります。

```bash:ターミナル1
ROOTFS=$(mktemp -d)
CID=$(sudo docker container create bash)
sudo docker container export $CID | tar -x -C $ROOTFS
ln -s /usr/local/bin/bash $ROOTFS/bin/bash
sudo docker container rm $CID
```

続いて、CGroupを設定します。参考記事と同様の制限をかけます。

```bash:ターミナル1
UUID=$(uuidgen)
sudo cgcreate -t $(id -un):$(id -gn) -a $(id -un):$(id -gn) -g cpu,memory:$UUID
cgset -r memory.limit_in_bytes=10000000 $UUID
cgset -r cpu.cfs_period_us=1000000 $UUID
cgset -r cpu.cfs_quota_us=300000 $UUID
```

そして、GestOS上にブリッジを作成し、コンテナに割り当てる用のvethも作成しておきます。

```bash:ターミナル1
## to create bridge
sudo ip link add name br0 type bridge
sudo ip addr add 10.10.0.1/24 broadcast 10.10.0.255 label br0 dev br0
sudo ip link set dev br0 up

## to create veth
sudo ip link add veth1 type veth peer name veth1_peer
sudo ip link set dev veth1 master br0
sudo ip link set dev veth1 up
```

これを実行すると下記のようになります。

```bash
lesson@lesson-VirtualBox:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:11:36:26 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.10/24 brd 192.168.10.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::74ed:aa40:24d9:f457/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
5: br0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether 96:f5:9c:89:f3:17 brd ff:ff:ff:ff:ff:ff
    inet 10.10.0.1/24 brd 10.10.0.255 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::d8bf:f3ff:fe59:620a/64 scope link 
       valid_lft forever preferred_lft forever
6: veth1_peer@veth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 1a:12:a0:d6:de:ec brd ff:ff:ff:ff:ff:ff
7: veth1@veth1_peer: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc noqueue master br0 state LOWERLAYERDOWN group default qlen 1000
    link/ether 96:f5:9c:89:f3:17 brd ff:ff:ff:ff:ff:ff
```

そしてようやく、紹介したコマンドを利用してコンテナを作成します。

```bash:ターミナル1
CMD="/bin/sh"
cgexec -g cpu,memory:$UUID \
  unshare -muinpfr /bin/sh -c "
    mount -t proc proc $ROOTFS/proc &&
    touch $ROOTFS$(tty); mount --bind $(tty) $ROOTFS$(tty) &&
    touch $ROOTFS/dev/pts/ptmx; mount --bind /dev/pts/ptmx $ROOTFS/dev/pts/ptmx &&
    ln -sf /dev/pts/ptmx $ROOTFS/dev/ptmx &&
    touch $ROOTFS/dev/null && mount --bind /dev/null $ROOTFS/dev/null &&
    /bin/hostname $UUID &&
    exec capsh --chroot=$ROOTFS --drop=cap_sys_chroot -- -c 'exec $CMD'
   "
```

上記を実行すると下記のようにPIDやネットワーク、filesystemが隔離されたコンテナが作成できます。

```bash:ターミナル1
# ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   13 root      0:00 ps
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# ls
bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var
```

現状ではloopbackのネットワークしかありません。そのためコンテナは作成できましたが、GestOS-Container間の通信はすることができません。
次に、GestOSからContainerのNetwork Namespaceを閲覧できるようにします。

```bash:ターミナル2
## コンテナを起動してるPIDを取得します。
lesson@lesson-VirtualBox:~$ ps -h
   3005 pts/0    Ss     0:00 -bash
   3206 pts/1    Ss     0:00 -bash
   3357 pts/0    S      0:00 unshare -muinpfr /bin/sh -c      mount -t proc proc /tmp/tmp.mBNY4xblZw/proc &&     touch /tmp/tmp.mBNY4xblZw/dev/pts/1; mount --bind /dev/pts/1 /tmp/tmp.mBNY4xblZw/dev/pts/1 &&     touch /tmp/tmp.mBNY4xblZw/d
   3358 pts/0    S+     0:00 /bin/sh
   3373 pts/1    R+     0:00 ps -h
## PIDからNetwork Namespaceを閲覧できるようにします。
lesson@lesson-VirtualBox:~$ sudo mkdir -p /var/run/netns
lesson@lesson-VirtualBox:~$ sudo ln -s /proc/3357/ns/net /var/run/netns/container01
lesson@lesson-VirtualBox:~$ ip netns list
container01
```

これでターミナル2からターミナル1で作成したNetwork Namespaceが閲覧できるようになります。
`ip netns list`で表示されるNamespaceは`/var/run/netns/`にあるディレクトリであるため、コンテナを起動しているPID先からリンクを貼ってあげると閲覧できます。mountでも試してみたんですが、lnだとうまくいきました。

そして、見えたNetwork Namespaceに対して`veth1`を設定し、ターミナル1でethを立ち上げます。

```bash:ターミナル2
lesson@lesson-VirtualBox:~$ sudo ip link set dev veth1_peer netns container01
lesson@lesson-VirtualBox:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:11:36:26 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.10/24 brd 192.168.10.255 scope global noprefixroute enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::74ed:aa40:24d9:f457/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
5: br0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
    link/ether 96:f5:9c:89:f3:17 brd ff:ff:ff:ff:ff:ff
    inet 10.10.0.1/24 brd 10.10.0.255 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::d8bf:f3ff:fe59:620a/64 scope link 
       valid_lft forever preferred_lft forever
7: veth1@if6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue master br0 state LOWERLAYERDOWN group default qlen 1000
    link/ether 96:f5:9c:89:f3:17 brd ff:ff:ff:ff:ff:ff link-netns container01
```

```bash:ターミナル1
# ip link set dev veth1_peer name eth0
# ip addr add 10.10.0.10/24 dev eth0
# ip link set dev eth0 up
# ip route add default via 10.10.0.1 
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 1a:12:a0:d6:de:ec brd ff:ff:ff:ff:ff:ff
    inet 10.10.0.10/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::1812:a0ff:fed6:deec/64 scope link 
       valid_lft forever preferred_lft forever
```

はい、ここまででLinuxの機能を利用しコンテナを起動、そのコンテナに対してNetworkを設定しました。
Networkの設定以外は参考記事を大きく参照しています。
ぜひ、手元で自作コンテナを作ってみてください!!

```bash:ターミナル1
# ping 10.10.0.1
PING 10.10.0.1 (10.10.0.1): 56 data bytes
64 bytes from 10.10.0.1: seq=0 ttl=64 time=0.158 ms
64 bytes from 10.10.0.1: seq=1 ttl=64 time=0.077 ms
64 bytes from 10.10.0.1: seq=2 ttl=64 time=0.077 ms
^C
--- 10.10.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.077/0.104/0.158 ms
```

```bash:ターミナル2
lesson@lesson-VirtualBox:~$ ping 10.10.0.10
PING 10.10.0.10 (10.10.0.10) 56(84) bytes of data.
64 bytes from 10.10.0.10: icmp_seq=1 ttl=64 time=0.123 ms
64 bytes from 10.10.0.10: icmp_seq=2 ttl=64 time=0.385 ms
64 bytes from 10.10.0.10: icmp_seq=3 ttl=64 time=0.050 ms
^C
--- 10.10.0.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2035ms
rtt min/avg/max/mdev = 0.050/0.186/0.385/0.143 ms
```
GestOS-コンテナ間での通信できた:raised_hands::raised_hands::raised_hands:

[^1]: エンジニアHub -コンテナ技術入門 https://eh-career.com/engineerhub/entry/2019/02/05/103000#
[^2]: Redhat -コンテナとVM　https://www.redhat.com/ja/topics/containers/containers-vs-vms