Containerd

【containerd】
containerd アーキテクチャについて
github.com

概要:
containerdは2016ローンチ。Dockerのプロジェクトではない。完全に別のPJとしてスタート。
現在dockerとlubernetesが正式にサポート。
Kubernetesがコンテナーを動作させるために必要な機能だけをサポートしているのでDokerより起動が早い?一部のコードはdockerのコードをそのまま利用しているらしい(どこかは不明)。

Dockerと比べてアーキテクチャはとてもシンプル。
スナップショット、メタデータ(タグとか)ストア、コンテンツストアだけ。

Containerdの内部構造(サブシステム単位の紹介)
バンドルとランタイムのサブシステムから構成される。

・バンドル:起動するコンテナーの設定情報、メタデータ(例:タグ名)、ルートファイルシステムデータを含む。
・ランタイム:バンドルを実行し、コンテナーを起動する。

Containerdを外部から操作するためには、GRPC APIプロトコルに従ってAPIを発行する。

Containerdの内部構造(サーバーサイドコンポーネント単位の紹介。)

・Executor:コンテナーランタイムを実際に実行する機能を提供。
・Supervisor:コンテナーの状態を監視、レポートする。(何をどこにレポートするのだろう?あとレポートにはログは含まれるのかな?)
・Metadata:グラフデータベース(何だろう?)にメタデータを格納する。バンドルについての参照情報を格納する。
・Content:コンテンツへのアクセスを提供する。コンテンツはここに格納されており、コンテンツハッシュをキーにアクセスすることができる。
・Snapshot:コンテナーイメージに対してファイルシステムのスナップショットを取得する機能を提供する。dockerのgraphdriverに似ているとのこと。(何だろう?) Layers are unpacked into snapshots.
・ Events:イベントを収集する機能を提供。
・Metrics:各コンポーネントはいくつかのメトリックを提供し、メトリックのAPIからアクセスできる。(将来的にサブシステムへの昇格させたいらしい)

以下のコンポーネントはクライアントサイド。
・Distribution:イメージのpushとpullが可能。

f:id:sota0113:20190514143620p:plain


バンドルは中央管理システムとして動作するcontainerdのコアであり、ランタイムはバンドルを動作するためのサブシステム。
Distributionはイメージのpull/pushを行うためのクライアントサイドコンポーネントである。


イメージプルのAPIリクエスト送付シナリオ。

①Distributionコンポーネントに対してイメージpullリクエストを送る。Distributionコンポーネントは取得したイメージをContentに格納し、イメージ名とイメージの格納場所をMetadataストアに格納。
②イメージが取得できたらbundle controllerに対してContentにあるイメージをbundleに展開するようにリクエストを送る。イメージはContentから取得し、イメージのレイヤーはsnapshotに格納される。
③スナップショットにファイルシステムが格納されたら、bundle controllerはそのイメージを利用してコンテナーを立てる準備ができました。
④bundleは、runtimeサブシステムに実行を依頼。runtimeサブシステムはbundleの設定を読み込み、コンテナーを起動する。



Getting-Started
https://github.com/containerd/containerd/blob/master/docs/getting-started.md

Getting Startedをやってみる
Containerdを実行する方法は様々ある。ctrツールでクイックにテストできるらしい。
別のプロジェクトにcontainerdを統合する形で使いたいなら、用意したパッケージを使うと良い。
getting startedでは、redisサーバーを動作させる。
環境はruncがビルドできる最近のlinuxを想定。
Goは1.9.xより上のバージョンを想定。

まずはgoとruncを入れるところまでやる。
Goから導入。
https://golang.org/dl/ からLinuxを選択してDL。VMに転送。

~/Downloads ❯❯❯ scp -P 3000 ./go1.12.4.linux-amd64.tar.gz root@127.0.0.1:~/
root@127.0.0.1's password:
go1.12.4.linux-amd64.tar.gz                                                                                                                                                                             100%  122MB  40.6MB/s   00:03
~/Downloads ❯❯❯ ssh -p 3000 root@127.0.0.1
root@127.0.0.1's password:
Last login: Fri Apr 19 21:21:23 2019 from 10.0.2.2
[root@localhost ~]#
[root@localhost ~]#
[root@localhost ~]# ls
anaconda-ks.cfg  c-compiler  case_test.sh  gVisor  go1.12.4.linux-amd64.tar.gz  if_test.sh
[root@localhost ~]#
[root@localhost ~]# tar -C /usr/local -xzf go1.12.4.linux-amd64.tar.gz
[root@localhost ~]# export PATH=$PATH:/usr/local/go/bin
[root@localhost ~]# which go
/usr/local/go/bin/go
[root@localhost ~]#
[root@localhost ~]# go version
go version go1.12.4 linux/amd64
[root@localhost ~]#

続いてrunc導入。

https://github.com/opencontainers/runc#building
runcとは、OCI(Open Container Initiative)にしたがってコンテナーを作成し動かすためのCLIツール。
runcはlinuxをサポート。goは1.6以上。
Seccompサポートを有効に?するためには、libseccompを導入する必要がある。Seccompのサポートなしでビルドするためには、makeコマンド実行時にBUILDYAGS=““を設定。
Seccomp→あった。

[root@localhost runc]# yum list installed | grep libseccomp
libseccomp.x86_64                    2.3.1-3.el7                       @base
[root@localhost runc]#


[root@localhost ~]# mkdir -p github.com/opencontainers
[root@localhost ~]# cd github.com/opencontainers
[root@localhost opencontainers]# git clone https://github.com/opencontainers/runc
Cloning into 'runc'...
remote: Enumerating objects: 2, done.
remote: Counting objects: 100% (2/2), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 19363 (delta 1), reused 0 (delta 0), pack-reused 19361
Receiving objects: 100% (19363/19363), 7.21 MiB | 571.00 KiB/s, done.
Resolving deltas: 100% (12540/12540), done.
[root@localhost opencontainers]# cd runc
[root@localhost runc]# ls
CONTRIBUTING.md  MAINTAINERS           NOTICE         VERSION        create.go  events.go  kill.go       main.go           pause.go    rlimit_linux.go    script              signals.go  state.go  update.go       vendor
Dockerfile       MAINTAINERS_GUIDE.md  PRINCIPLES.md  checkpoint.go  delete.go  exec.go    libcontainer  man               ps.go       rootless_linux.go  signalmap.go        spec.go     tests     utils.go        vendor.conf
LICENSE          Makefile              README.md      contrib        docs       init.go    list.go       notify_socket.go  restore.go  run.go             signalmap_mipsx.go  start.go    tty.go    utils_linux.go
[root@localhost runc]#

[root@localhost runc]# cp Makefile Makefile.org
[root@localhost runc]# vi Makefile
[root@localhost runc]# diff Makefile Makefile.org
15c15
< BUILDTAGS ?= ""
---
> BUILDTAGS ?= seccomp
[root@localhost runc]#

エラーに。(seccompあったので、上のMakefileいじる作業は、なしで。。。)

[root@localhost ~]# pwd
/root
[root@localhost ~]# mv github.com/ /usr/local/go/src/
[root@localhost ~]# cd /usr/local/go/src/
[root@localhost src]# pwd
/usr/local/go/src
[root@localhost src]#
[root@localhost runc]# pwd
/usr/local/go/src/github.com/opencontainers/runc
[root@localhost runc]# make
go build -buildmode=pie  -ldflags "-X main.gitCommit="029124da7af7360afa781a0234d1b083550f797c-dirty" -X main.version=1.0.0-rc7+dev " -tags """" -o runc .
[root@localhost runc]# make install
install -D -m0755 runc /usr/local/sbin/runc
[root@localhost runc]#

Build Tags
という項目(Makefileの中身の項目)で、オプションが指定できる。

Running the test suite

runcランタイムはDocker経由でテストができるようだ。

[root@localhost runc]# systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: https://docs.docker.com
[root@localhost runc]# systemctl start docker
[root@localhost runc]# make test
make unittest integration rootlessintegration
make[1]: ディレクトリ `/usr/local/go/src/github.com/opencontainers/runc' に入ります
docker build  -t runc_dev:master .
Sending build context to Docker daemon  29.72MB
Step 1/13 : FROM golang:1.10-stretch
1.10-stretch: Pulling from library/golang
741437d97401: Downloading [===========>                                       ]  10.57MB/45.34MB
34d8874714d7: Download complete
0a108aa26679: Download complete
7f0334c36886: Downloading [===========>                                       ]  11.19MB/50.06MB
d35724ed4672: Downloading [=>                                                 ]  2.149MB/57.62MB
c0eaf021aeaf: Waiting
d3d9c96611f1: Waiting
……
….
..

Using runc
OCI規格でバンドルされたコンテナーが必要。
OCI規格とは?

======(OCIについてここから)======
公式サイト
https://www.opencontainers.org/
OCIとは、Open Container Initiativeの略で、2015年にDockerほかによって設立された、コンテナー分野の標準化を図る組織。
大きく2つの目的があり、runtimeの標準化と、imageの標準化を目的として設立。
runtimeの標準化:どのように、ディスク上でunpackされているファイルシステムのひとかたまり(バンドル)を動かすか。
コンテナーイメージをダウンロードしてきて、それをOCIランタイムファイルシステムバンドルとしてunpackし、OCIランタイムによって動作させるイメージ。

ファイルシステムバンドルって?
https://github.com/opencontainers/runtime-spec/blob/master/bundle.md
上のリンクは、コンテナーをファイルシステムバンドルにエンコードするフォーマットについて。
ファイルシステムバンドル」は、ある決まりに従って、まとめられたファイルの束のこと。
このファイルの束は、コンテナーの中身に直接的に関係のあるファイルと、コンテナーのランタイムが標準的な決まり(standard operation)に従ってコンテナーを動作させるために必要なデータが含まれる。
ここでの「バンドル」という単語は、コンテナーとその構成データが、ランタイムに利用されるために、ローカルのファイルシステムにどのように保存されるかを表している。

通常のコンテナーのバンドルは、具体的に以下より構成される。
1. config.json:コンテナの構成データ
2. コンテナーのルートファイルシステム:root.pathで参照されるデータ。(config.jsonで設定されている場合の話。設定されていないと??)

これらのファイルはローカルファイルシステム内の単一の、あるディレクトリに存在する必要があります。そのディレクトリ自体はバンドルの一部として含めてはいけません。言い換えると、これらのファイルがアーカイブのルートに存在するべきなのです。

さて、上で、コンテナーは標準的な決まりに従ってコンテナーを動作させる、とありました。
この標準的な決まりとはなんでしょうか?
それは以下に記述されています。
https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#operations
コンテナの状態については必ずいくつかの情報を提供する必要があります。
その提供すべき情報と、コンテナーのライフサイクルについて規定されています。

まず先に、コンテナーのライフサイクルを確認しましょう。
コンテナーのライフサイクルは、コンテナーが生成されてからexitするまでの間において、全部で10ステップに定義されます。

1.OCI準拠のランタイムのコンテナー作成のcreateコマンドが、ファイルシステムバンドルの場所と、ユニークな(コンテナーの)IDと共に呼び出されます。
2.コンテナーのランタイムはconfig.jsonに従ってコンテナーを作成します。もしconfig.jsonに従ってコンテナーが作成できなかった場合、エラー状態(????)とする必要があります。一方でランタイムは、この次のステップ3.で必要なconfig.jsonのprocessで指定されている動作は実行しない必要があります。このステップ以降でconfig.jsonが変更されたとしても、その変更は反映させない必要があります。
3.ランタイムのstartコマンドが呼び出されます。この時ユニークなコンテナーIDとともにコマンドが呼び出されます。
4.ランタイムからprestat hooksが呼び出されます。失敗すると、エラー状態(????)となる必要があります。その後、コンテナーを停止させ、ステップ9を実行します。
5. config.jsonのユーザーによって定義されたprocessの内容を実行します。
6.ランタイムからpoststart hooksが呼び出されます。失敗すると、警告をログに表示します。その後、残っているhook処理やライフサイクルは継続的に実行されます。
7.コンテナーのプロセスが終了(exit)します。エラーやクラッシュ、意図的なexit、ランタイムのkillオペレーションが呼び出されてexitします。
8.ランタイムのdeleteコマンドが呼び出されます。この時、コンテナーのユニークなIDと共にコマンドが呼び出されます。
9.ステップ2で実行した内容を元に戻して、コンテナーが消去されます。
10.ランタイムからpoststop hooksが呼び出されます。hooksが失敗した場合、警告をログに表示します。後続処理は継続的に実行されます。

以上の10ステップがOCIで取り決められているコンテナーのライフサイクルです。
コンテナーの状態の話に戻ります。
コンテナの状態については、必ず以下の情報を提供する必要があります。

“ociVersion” (String,必須):OCIのバージョン。(OCIのバージョンって?)
“Id”(String,必須):コンテナーID。ホスト内で必ずユニークでなくてはいけない。ホスト間ではユニークである必要はない。
“Status”(String,必須):コンテナーのランタイムの状態。以下の状態が好ましい。(mayであり、mustではない。)
creating:コンテナーが作成中であることを表します。(先ほどのライフサイクルのステップ2ですね)
created:コンテナー作成オペレーションが完了したことを表します。(先ほどのライフサイクルのステップ2の直後ですね。)このとき、コンテナープロセスが終了していなく、ユーザーが指定したプログラム(config.jsonのprocessの項目)が実行されていない状態である必要があります。
running:ユーザーが指定したプログラム(config.jsonのprocessの項目)が実行されており、exitしていない状態を表します。(先ほどのライフサイクルのステップ5ですね)
stopped:プロセスが終了したことを表します。(先ほどのライフサイクルのステップ7ですね)
上述の4つの状態のほかにも、任意で状態を追加設定できます。ただし、上の4つを置き換えるような状態ではないことが条件です。
“pid”(int,必須):

“Bundle”(String,必須)

“annotation”(map, オリジナル)

config.jsonには、様々なコンテナーの構成情報を設定できます。
https://github.com/opencontainers/runtime-spec/blob/master/config.md
上のリンクはconfig.jsonの概要。

先に上のコンテナーライフサイクルのステップ2.で出てきたprocessについて紹介します。
Process自体は、オプショナルの項目ですがstartコマンド実行時には必要な項目になります。


======(OCIについてここまで)======


Dockerの exportメソッドで既存のdockerコンテナからルートファイルシステムを得られる。

[root@localhost runc]# pwd
/usr/local/go/src/github.com/opencontainers/runc
[root@localhost runc]# cd
[root@localhost ~]# ls
anaconda-ks.cfg  c-compiler  case_test.sh  gVisor  go1.12.4.linux-amd64.tar.gz  if_test.sh
[root@localhost ~]#
[root@localhost runc]# mkdir ./mycontainer
[root@localhost runc]# cd ./mycontainer
[root@localhost mycontainer]# mkdir rootfs
[root@localhost mycontainer]# docker export $(docker create busybox) | tar -C rootfs -xvf -
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
fc1a6b909f82: Pulling fs layer
fc1a6b909f82: Verifying Checksum
fc1a6b909f82: Download complete
fc1a6b909f82: Pull complete
Digest: sha256:954e1f01e80ce09d0887ff6ea10b13a812cb01932a0781d6b0cc23f743a874fd
Status: Downloaded newer image for busybox:latest
.dockerenv
bin/
………
……
…
..
var/www/
[root@localhost mycontainer]# ls
rootfs
[root@localhost mycontainer]# ls rootfs/
bin  dev  etc  home  proc  root  sys  tmp  usr  var
[root@localhost mycontainer]#

ルートファイルシステムが生成できたら、バンドルの中にスペックを示すconfig.jsonを作成する。
雛形はrunc specコマンドで作成される。

[root@localhost mycontainer]# ls
rootfs
[root@localhost mycontainer]# runc spec
[root@localhost mycontainer]# ls
config.json  rootfs
[root@localhost mycontainer]#

Config.jsonの例を示す大元のGitHub
https://github.com/opencontainers/runtime-spec/blob/master/config.md#platform-specific-configuration
その中のPlatform-specific configurationでプラットフォームごとの設定項目へのリンクがある。
Linuxの場合、Config.jsonのパラメータは以下を見て変更できる。(namespace、cgroup、)
https://github.com/opencontainers/runtime-spec/blob/master/config-linux.md


Config.jsonは一旦デフォルトでいいかな?
とりあえず動かしたい

Running Containers
https://github.com/opencontainers/runc#running-containers

[root@localhost mycontainer]# ls
config.json  rootfs
[root@localhost mycontainer]# runc run mycontainerid
/ #
/ # pwd
/
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # exit
[root@localhost mycontainer]#

Runc specコマンドによるconfig.jsonファイルがデフォルトのままであれば、上のように、shのセッションが起動する。

Config.jsonを少しいじってみる。以下の通りterminalをfalse、argsをsleep 5に変更。

        "process": {
                "terminal": false,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sleep", "5"
                ],

createする。

[root@localhost mycontainer]# runc create mycontainerid
Listメソッドでコンテナー一覧。
[root@localhost mycontainer]# runc list
ID              PID         STATUS      BUNDLE                   CREATED                          OWNER
mycontainerid   8515        created     /root/runc/mycontainer   2019-04-19T15:15:38.859156643Z   root
[root@localhost mycontainer]# ps -aux | grep 8515
root      8515  0.0  1.3 124096 13120 ?        Ssl  00:15   0:00 runc init
root      8526  0.0  0.2 112732  2364 pts/1    R+   00:15   0:00 grep --color=auto 8515
[root@localhost mycontainer]#


[root@localhost mycontainer]# runc start mycontainerid
[root@localhost mycontainer]# runc list
ID              PID         STATUS      BUNDLE                   CREATED                          OWNER
mycontainerid   8515        running     /root/runc/mycontainer   2019-04-19T15:15:38.859156643Z   root
[root@localhost mycontainer]# runc list
ID              PID         STATUS      BUNDLE                   CREATED                          OWNER
mycontainerid   0           stopped     /root/runc/mycontainer   2019-04-19T15:15:38.859156643Z   root
[root@localhost mycontainer]#

コンテナーライフサイクル
https://github.com/containerd/containerd/blob/master/design/lifecycle.md
スナップショットのしくみ
https://github.com/containerd/containerd/blob/master/design/snapshots.md
データフローについて
https://github.com/containerd/containerd/blob/master/design/data-flow.md
マウントについて
https://github.com/containerd/containerd/blob/master/design/mounts.md


マルチテナンシーをどのように実現しているか
https://github.com/containerd/containerd/blob/master/docs/namespaces.md



Rootlessで動作させるには。(rootlessとは何だろう??)
https://github.com/containerd/containerd/blob/master/docs/rootless.md

Goで書いてcontainerdを操作してみよう。
https://github.com/containerd/containerd/blob/master/docs/client-opts.md

Docker Con 2017 in USでディスカッションされたポイント。
https://github.com/containerd/containerd/blob/master/docs/dockercon-summit.md