cri-toolsでコンテナを起動する

## この記事で書くこと

cri-tools(`crictl`)でContainer Runtimeを操作してContainerを起動してみる.

- High level Runtime: containerd
- Low level Runtime: runc

## 環境構築

#### 利用環境

以下の仮想マシンを用います:

- OS: Ubuntu18.04
- Infra: AWS EC2 t3a.nano

#### パッケージインストール

Runtimeをインストールしていきます.

containerd/runcは[Dockerをインストールする手順](https://docs.docker.com/engine/install/ubuntu/)をなぞって `containerd.io` のみインストールするようにします.

<details>
<summary>コマンド</summary>
<div>

```sh
sudo apt purge -y docker docker-engine docker.io containerd runc
sudo apt update -y
sudo apt install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    | sudo apt-key add -
sudo add-apt-repository \
    "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) \
    stable"
sudo apt update -y
sudo apt install containerd.io
```

参考:

- [Dockerインストール手順](https://docs.docker.com/engine/install/ubuntu/)
- [kubernetes.ioのContainer runtimes](https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd)
    - network周りは後の[設定](#設定)にまとめて書きますがやっておいてもOK
</div>
</details>

関連ツールとしてCLIであるcrictlとcniプラグインをインストールします.

[CRI-O](https://cri-o.io)や[podman](https://podman.io)のパッケージをホストしているKubicリポジトリからインストールします.　具体的には[CRI-Oのインストール手順](https://cri-o.io/#distribution-packaging)で `cri-tools` と `containernetworking-plugins` パッケージのみインストールします.

<details>
<summary>コマンド</summary>
<div>

```sh
export OS=xUbuntu_18.04
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" \
    | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key \
    | sudo apt-key add -
sudo apt update -y
sudo apt install -y \
    cri-tools \
    containernetworking-plugins
```

参考:

- [CRI-Oのインストール手順](https://cri-o.io/#distribution-packaging)
    - (ほぼ同内容の)[kubernetes.ioでの紹介](https://kubernetes.io/docs/setup/production-environment/container-runtimes/#cri-o)
- [podmanのインストール手順](https://podman.io/getting-started/installation#ubuntu)

</div>
</details>

#### 設定

kubernetes.ioの[Container runtimesの手順](https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd)に従ってOSのネットワーク設定を行います.

<details>
<summary>コマンド</summary>
<div>

```sh
cat << __EOT__ | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
__EOT__

sudo modprobe overlay
sudo modprobe br_netfilter

cat << __EOT__ | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
net.bridge.bridge-nf-call-ip6tables = 1
__EOT__

sudo sysctl --system
```

</div>
</details>

また, containerd自体の設定を作ってcontainerdサービスを再起動しておきます.

```sh
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
sudo systemctl restart containerd
```

さらにcniの設定として `/etc/cni/net.d/50-bridge.conflist` を以下の内容[^1]で作成します.

```json:/etc/cni/net.d/50-bridge.conflist
{
  "cniVersion": "0.4.0",
  "name": "cni-bridge",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni-bridge0",
      "isGateway": true,
      "isMasq": true,
      "hairpinMode": true,
      "ipam": {
        "type": "host-local",
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ],
        "ranges": [
          [
            {
              "subnet": "10.66.0.0/16",
              "gateway": "10.66.0.1"
            }
          ]
        ]
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    },
    {
      "type": "firewall"
    },
    {
      "type": "tuning"
    }
  ]
}
```

`crictl` の設定を以下の内容で `/etc/crictl.yaml` に作っておきます.

```yaml:/etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
```

## Container実行 (シンプル版)

pod定義とcontainer定義の2つのjsonファイルを作っておきます.

```json:busybox-pod.json
{
  "metadata": {
    "name": "busybox",
    "namespace": "default",
    "attempt": 1,
    "uid": "hdishd83djaidwnduwk28bcsb"
  },
  "log_directory": "/tmp",
  "linux": {
  }
}
```

```json:busybox-container.json
{
  "metadata": {
    "name": "busybox"
  },
  "image":{
    "image": "busybox"
  },
  "command": [
    "top"
  ],
  "log_path":"busybox.0.log",
  "linux": {
  }
}
```

使用するContainer imageをpullしておきます.

```sh
sudo crictl pull busybox
```

`runp` コマンドでpodを作ります:

```sh
sudo crictl runp busybox-pod.json
```

作成されたpod idが以下のように出力されます.

```txt
454c57297c0f385fbbf6c4557179e399f8ebd8b17d4cc6e6632e11e745d0f59b
```

`pods` コマンドで作成されたpodを確認することができます.

```
sudo crictl pods --namespace default
```

出力されたpod idを使って `create` コマンドでpod内にContainerを作成します.

```
sudo crictl create <pod id> busybox-container.json busybox-pod.json
```

作成されたcontainer idが以下のように出力されます.

```txt
b67493102c7a10ac29c23f8bfdbf001cbf2ba0c368f818d85e0ab4189ef853cc
```

`start` コマンドで作成されたContainerを起動します.

```sh
sudo crictl start <container id>
```

`ps` コマンドで起動しているContainer(`-a` オプションで起動しないものも含める)を一覧できます.

```sh
sudo crictl ps
```

起動した状態では `exec` や `logs` といったおなじみのコマンドでContainer操作ができます. また, 今回のContainerは `top` コマンドを実行しているだけなので面白くありませんが, `port-forward` コマンドも使えます.

ContainerとPodの削除は以下の通りです. (Pod IDを `${podid}`, Container IDを `${containerid}` で参照します)

```sh
sudo crictl stop "${containerid}"
sudo crictl rm "${containerid}"
sudo crictl stopp "${podid}"
sudo crictl rmp "${podid}"
```

## Container実行 (nginx版)

前の例ではContainer内でコマンドを実行するだけだったので, 今度はnginxを起動してhostPortへのbindとhostPathをbindして静的HTMLファイルを返してみましょう.

以下のファイルを作成します.

```json:nginx-pod.json
{
  "metadata": {
    "name": "nginx",
    "namespace": "default",
    "attempt": 1,
    "uid": "hdishd83djaidwnduwk28bcsc"
  },
  "log_directory": "/tmp",
  "port_mappings": [
    {
      "protocol": 0,
      "container_port": 80,
      "host_port": 80
    }
  ],
  "linux": {
    "security_context": {
      "privileged": true
    }
  }
}
```

```json:nginx-container.json
{
  "metadata": {
    "name": "nginx"
  },
  "image":{
    "image": "nginx:1.19"
  },
  "mounts": [
    {
      "container_path": "/usr/share/nginx/html/",
      "host_path": "/home/ubuntu/html/"
    }
  ],
  "log_path":"nginx.0.log",
  "linux": {
    "security_context": {
      "privileged": true
    }
  }
}
```

レスポンスとする以下のhtmlファイルは, Mountの都合で `html/` ディレクトリを(上記 `nginx-container.json` で指定した `host_path` と一致するように)作成した中に作成してください.

```html:html/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Hello Container!</title>
  </head>
  <body>
    <h1>Hello, Container world!</h1>
    <img style="width: 100px;" src="https://raw.githubusercontent.com/opencontainers/artwork/master/oci/icon/color/oci-icon-color.svg">
  </body>
</html>
```

さて上記configを使ってnginxを起動し, hostの80番ポートにbindしましょう.

起動コマンドはシンプル版とほぼ変わらないので特に説明しませんが, 以下のようになります. (bashを想定しています)

```bash
podid="$(sudo crictl runp nginx-pod.json)"
containerid="$(sudo crictl create "${podid}" nginx-container.json nginx-pod.json)"
sudo crictl start "${containerid}"
```

curlコマンドでホストから `http://127.0.0.1` にアクセスすると先ほど作成したHTMLファイルの内容が返ってきていることがわかります.

以下はSSHポートフォワードで8080番ポートをVMの80番ポートにフォワードしてブラウザから閲覧した様子です.

![hello-container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147887/6ae9a6aa-056d-74f3-c3e4-172e7b996f38.png)

また, 以下のコマンドでContainer内のportにport-forwardすることもできます. (今回はhostPortにbindしているので不要ですが, `crictl` の機能として紹介しています)

```sh
sudo crictl port-forward "${podid}" 8080:80
```

シンプル版と同様にPod/Containerを削除できます.

## まとめ

今回はcontainerdとruncに対して `crictl` コマンドを使って, つまりCRIを使ってContainer操作を行ってみました.

おおよそdockerコマンドでよく行うことはできたと思っています.

今後はcontainerdをcri-oに, runcをcrunやrunscに変えて動かしてみたいと思っています.


[^1]: このcni設定は[podman](https://podman.io)をインストールするとpodmanが使うために用意する設定を ~~ほぼ丸パクリ~~ 参考にしてIP rangeや名前だけ変えたものになります. カスタムする際は [cni.devのplugins](https://www.cni.dev/plugins/) などを参考にするとよいでしょう.