チェシャ猫の消滅定理

数学にプログラミング、素敵なもの何もかも。

猫でもわかる rkt + Kubernetes

このエントリは Kubernetes Advent Calendar 2017 の 23 日目の記事です。ちなみに昨日は takezaki さんの「GCBを利用したContinuous Delivery環境」でした。

LT で使用したスライド

先日、市ヶ谷Geek★Night #16 の 10 分 LT 枠で、CoreOS 社によるコンテナ実装 rkt とその Kubernetes 連携について発表してきました。今回のエントリはこの LT の内容を補足しつつ、実際に手を動かして rkt を試せるような構成にしてあります。

Hello, rkt!

rkt は、Docker の対抗馬として CoreOS によって開発されたコンテナ管理ツールです。プロジェクトの初期は普通に Rocket という名前でコマンド名が rkt という扱いでしたが、途中からツールの名前自体が rkt に変更されました。

github.com

現在は Cloud Native Computing Foundation (CNCF) にプロジェクトごと寄贈されていますが、メンテナ を確認すると 11 人中 8 人が CoreOS 所属で、実際には依然として CoreOS が中心となっているようです。ちなみに残りの 3 人は Kinvolk 社 所属で、後で登場する rktlet の開発元でもあります。

rkt のインストール

Go で作られたツールの常として、rkt もインストールは簡単です。

Linux の場合、各ディストリビューション向けのパッケージ配布もされていますが、触ってみるだけであれば直接バイナリをダウンロードすればすぐ試せます。

この記事の後半で rkt app コマンドを使用するためには現時点の最新版である v1.29.0 が必要なのでそこだけ注意してください。

$ wget https://github.com/rkt/rkt/releases/download/v1.29.0/rkt-v1.29.0.tar.gz
$ tar xzvf rkt-v1.29.0.tar.gz
$ sudo mv rkt-v1.29.0/rkt /usr/local/bin/rkt
$ rkt version
rkt Version: 1.29.0
appc Version: 0.8.11
Go Version: go1.8.3
Go OS/Arch: linux/amd64
Features: -TPM +SDJOURNAL

残念ながら rkt は Linux 専用です。rkt のレポジトリが Vagrantfile を提供しているので、MacWindows の人はこれで VM を立ち上げれば大丈夫です。

git clone https://github.com/rkt/rkt
cd rkt
vagrant up

特徴その1:デーモンが存在しない

Docker の場合、docker コマンドを使い始める前に Docker Engine をサービスとして登録・起動する作業(実際にはパッケージマネージャがやってくれると思いますが)が必要です。

rkt の場合 Docker Engine に相当するコンポーネントがないため、ダウンロードしたバイナリだけでツールとして完結しています。 実際にちょっと触ってみましょう。

Docker と少しだけ異なる点として、rkt ではイメージに署名が付けられています。信頼した公開鍵は /etc/rkt/trustedkeys 以下に記録されています。

$ sudo rkt trust --prefix coreos.com/etcd
...
$ tree /etc/rkt/trustedkeys/
/etc/rkt/trustedkeys/
└── prefix.d
    └── coreos.com
        └── etcd
            ├── 18ad5014c99ef7e3ba5f6ce950bdd3e0fc8a365e
            └── 8b86de38890ddb7291867b025210bd8888182190

あとの操作は Docker とほぼ同じです。イメージをダウンロードしてからコンテナを立ち上げて消すまでの一連の流れを試してみましょう。

$ sudo rkt fetch coreos.com/etcd:v3.1.7
...
image: signature verified:
  CoreOS Application Signing Key <security@coreos.com>
...
$ sudo rkt image list
ID                     NAME                      SIZE     IMPORT TIME       LAST USED
sha512-e7a54697d04d    coreos.com/etcd:v3.1.7    58MiB    43 seconds ago    43 seconds ago
$ sudo rkt run coreos.com/etcd:v3.1.7

(別ターミナルで)
$ sudo rkt list
UUID        APP     IMAGE NAME                STATE      CREATED         STARTED         NETWORKS
a79a0bd6    etcd    coreos.com/etcd:v3.1.7    running    1 minute ago    1 minute ago    default:ip4=172.16.28.2
$ sudo rkt stop a79a0bd6
"a79a0bd6-4260-479d-993d-08f032781007"
$ sudo rkt list
UUID        APP     IMAGE NAME                STATE     CREATED           STARTED           NETWORKS
a79a0bd6    etcd    coreos.com/etcd:v3.1.7    exited    12 minutes ago    12 minutes ago
$ sudo rkt rm a79a0bd6
"a79a0bd6-4260-479d-993d-08f032781007"
$ sudo rkt list
UUID    APP    IMAGE NAME    STATE    CREATED    STARTED    NETWORKS

Docker Hub にホストされているイメージを立ち上げることも可能で、イメージの指定は docker://IMAGE:TAG という形式になります。この場合は署名の検証ができないので --insecure-options=image を付けます。

$ sudo rkt run --insecure-options=image docker://redis:4.0.6
$ sudo rkt list
UUID        APP      IMAGE NAME                                  STATE      CREATED           STARTED           NETWORKS
2df6d5ee    redis    registry-1.docker.io/library/redis:4.0.6    running    11 seconds ago    11 seconds ago    default:ip4=172.16.28.2
(以下同様)

特徴その2:Pod が最初からサポートされている

もう一つ、Docker に対する rkt の特徴として挙げられるのは Pod のサポートです。上の例ではコンテナが一つしかないのではっきりとわかりませんが、実は UUID はコンテナではなく Pod に対して振られています。

あまり意味のある組み合わせではありませんが、先ほど fetch した etcdredis を同じ Pod 内で起動させてみましょう。

$ sudo rkt run coreos.com/etcd:v3.1.7 docker://redis:4.0.6

(別ターミナルで)
$ sudo rkt list
UUID        APP      IMAGE NAME                                  STATE      CREATED          STARTED          NETWORKS
1f752f5c    etcd     coreos.com/etcd:v3.1.7                      running    8 seconds ago    8 seconds ago    default:ip4=172.16.28.2
            redis    registry-1.docker.io/library/redis:4.0.6

Pod 1f752f5c の内部で etcdredis が立ち上がっている様子がわかります。

この二つのコンテナは、systemd の機能によって他から隔離されています。例えば以下のように ip netns list コマンドを実行してみましょう。Pod に対応する network namespace が作成されているのがわかるはずです。また、Pod を rm すると namespace も消えます。

$ ip netns list
cni-e0f06652-1a94-2cfd-3ccc-c684c473992f (id: 1)
$ sudo rkt stop 1f752f5c
"1f752f5c-6046-4458-8105-832227a07cfa"
$ sudo rkt rm 1f752f5c
"1f752f5c-6046-4458-8105-832227a07cfa"
$ ip netns list
(出力なし)

さて、ここでもう一度イメージの一覧を表示してみましょう。

$ sudo rkt image list
ID                     NAME                                        SIZE      IMPORT TIME   LAST USED
sha512-e7a54697d04d    coreos.com/etcd:v3.1.7                      58MiB     1 hour ago    1 hour ago
sha512-e50b77423452    coreos.com/rkt/stage1-coreos:1.29.0         211MiB    1 hour ago    1 hour ago
sha512-eb03c7ec1861    registry-1.docker.io/library/redis:4.0.6    204MiB    1 hour ago    1 hour ago

立ち上げた覚えのないイメージ coreos.com/rkt/stage1-coreos:1.29.0 がいつの間にか fetch されていますね。このイメージこそ、rkt がネイティブで Pod をサポートできる仕掛けです。

rkt のアーキテクチャでは、コンポーネントは三つの役割に階層化されています。先ほど rkt コマンドを操作した際は目立ちませんでしたが、ユーザの操作とアプリケーションコンテナの起動の間に Stage1 が入っているのがポイントです。Stage1 は Pod の実装を提供し、Pod 内のコンテナを他のプロセスから隔離します。

  • Stage0:ユーザと直接やりとりする
  • Stage1:Pod の実装を提供する。Stage0 から呼び出される
  • Stage2:コンテナ化されたアプリケーション本体。Stage1 から呼び出される

ここで面白いのは、Stage1 を切り替えることで、Pod ごとに隔離レベルを変えられる点です。rkt は以下の三つの実装を公式に提供しています 。

  • stage1-coreos:デフォルトの設定。systemd の namespace および cgroup を使用して隔離する
  • stage1-fly:より隔離レベルが低い設定。chrootファイルシステムのみを切り替え、namespace は隔離しない
  • stage1-kvm:より隔離レベルが高い設定。KVM を使用して Pod ごとに仮想マシンを作成し隔離する

使用する Stage1 の実装は、--stage1-name オプションで指定することができます。試しに fly による Pod 作成を試してみましょう。

fly による Pod を作成する場合、イメージ側で定義された volume に対して自動的にマウントポイントをつくる仕組みがうまく動作しないようです。ここでは --volume オプションで明示的に指定して起動させました。

$ sudo mkdir /mnt/etcd-data
$ sudo rkt run --stage1-name=coreos.com/rkt/stage1-fly:1.29.0 --volume data-dir,kind=host,source=/mnt/etcd-data coreos.com/etcd:v3.1.7

(別ターミナルで)
$ sudo rkt list
UUID        APP     IMAGE NAME                STATE      CREATED           STARTED           NETWORKS
307f689f    etcd    coreos.com/etcd:v3.1.7    running    16 seconds ago    16 seconds ago

先ほどの場合と異なり、NETWORK 欄が空のままになっていることに注目しましょう。これは network namespace が隔離されていないことによるもので、実際、Pod が running の状態にもかかわらず ip netns list コマンドを実行しても何も返ってきません。

Container Runtime Interface

rktnetes

さて、ここまで見てきたように、rkt には次のような特徴がありました。

  • Docker と違ってデーモンがなく、Linux 付属の systemd を使用
  • 複数のコンテナを Pod としてまとめて隔離する仕組みを持っている

これらの性質を見ると、いかにも Kubernetes(あるいはそれ以外のオーケストレータも)との相性が良さそうに見えます。

実際、rkt 側はもちろん Kubernetes 自身もそう考えていたようで、Kubernetes v1.3、Docker 以外の初のコンテナランタイムとして rkt 連携がサポートされました。時系列としては 2016 年 7 月のことです。

この rkt 連携機能は rktnetes と通称されていました。当時の Kubernetes 公式ブログ の表現を引用しましょう。

rktnetes is about more than just rkt. It’s also about refining and exercising Kubernetes interfaces, and paving the way for other modular runtimes in the future.

すなわち、rktnetes は Kubernetes とコンテナランタイムとの間のインターフェースを設計する上での先行事例として位置づけられていたことがわかります。

しかしその目的に反して、実装の中身はかなり乱暴です。Kubernetes のレポジトリ内、kubernetes/pkg/kubelet/rkt にある ソースコード を見ると、インタフェースを作ったというよりは、rkt のデータ構造や動作を Kubelet 側でも実装した、と表現したほうが近いように見えます。

CRI の登場

Kubernetes v1.5 でまた別の動きが現れます。Container Runtime Interface (CRI) の策定です。

CRI が定義する API.proto ファイル の形で提供されており、大きく分けて二つの gRPC サービスからできています。

  • ImageSerivce:イメージを操作するための API
  • RuntimeService:Pod やその中のコンテナを操作するための API

前者のイメージの操作については、それぞれ CRI に対応するコマンドが rkt 側にももともと存在するので大きな障害にはなりません。

問題は後者のコンテナ操作です。CRI には

  • Pod を作成する
  • Pod 内にコンテナを作成する
  • コンテナを起動する

といった逐次実行命令型のインタフェースが規定されています。しかし rkt の Pod は 基本的に起動後に状態を変更することができない、いわゆる immutable なつくりになっていて、そのままでは Pod の作成とコンテナの追加を独立して行うことができません。

CRI の策定に当たって Pod レベルの宣言的なインタフェースを避けた理由は、初期段階のプロポーザル で触れられています。

  1. すべてのランタイムが Pod をネイティブサポートしているとは限らず、対応負荷が大きくなるため
  2. インタフェースが抽象的すぎると、再起動や LifecycteHook のロジックを各ランタイム側で実装することになり重複が生じるため
  3. 開発速度の速さから、Pod の設定項目はすぐ変化する可能性があるため

後者二つはともかく、最初の要件は rkt 側からすれば完全に梯子を外された形です。

ちなみに、もちろん Docker はいち早く CRI に反応しました。それまで Kubernetes が Docker を呼び出すコードは kubernetes/pkg/kubelet/dockerutils にありましたが、新たに CRI 対応版である kubernetes/pkg/kubelet/dockershim が開発され、半年後である v1.7 の段階 では移行が完全に完了してdockerutils は姿を消しています。

一方 rkt はと言えば、実は Docker 同じように v1.5 の段階ですでに rktshim作成されて います。しかし中身を覗いてみると実装はなく単に panic が呼ばれるだけのスタブになっており、さらに残念なことに最新版 v1.9 になってもやはり 実装はされない まま放置されています。

rkt app サブコマンドと rktlet

さて、rkt もここで Kubernetes 連携を諦めたわけではありません。放置されている rktshim に代わって、新しいプロジェクト rktlet の開発が進められています。rktlet は Kubernetes Incubator プロジェクト の中の一つであり、中心となっているメンバは rkt のメンテナの件で言及した Kinvolk 社です。

github.com

rktlet では、以下の二つの変更によって CRI 対応を実現しようとしています。

まず、rkt そのものに手を入れることで、CRI に対応するために新たにPod に対して mutable な操作を行う rkt app サブコマンドが追加されました。

さらに、デーモンを持たないという哲学は妥協して、rktlet がデーモンとして起動し gRPC を待ち受ける仕組みになりました。ただしコンテナ操作のロジックをデーモンに持たせることはせず、あくまでも rkt を外部コマンドとして呼びます。

実際に触って試してみましょう。とはいえ Kubelet から呼ばれている最中の様子を確認するのは原理的に難しいので、まず手で rkt app コマンドを実行してみて、それから実際の Kubernetes との連携に進みます。

rkt app サブコマンド

rkt app サブコマンドを使用することで、もともと rkt ではできなかった Pod の mutable な操作が可能になります。

最新版の v1.29.0 でもまだ開発中のステータスなので、コマンド実行時には環境変数 RKT_EXPERIMENT_APP=true を指定しましょう。

まず空の Pod の作成です。CRI の RunPodSandbox に相当する操作です。

$ sudo RKT_EXPERIMENT_APP=true rkt app sandbox

(別ターミナルで)
$ sudo rkt list
UUID        APP    IMAGE NAME    STATE    CREATED    STARTED    NETWORKS
c77b3f3b    -    -    running    1 minute ago    1 minute ago    default:ip4=172.16.28.2
$ ip netns list
cni-a08f779f-ce1e-771b-b174-20ba662e6b7e (id: 0)

今までと異なり、APP に何も登録されていない Pod が作られました。しかし NETWORK 欄や ip netns list コマンドの結果を見ると、この段階ですでに namespace が先に作成されているのがわかります。

次に、この Pod の中にコンテナを追加してみます。CRI の CreateContainer + StartContainer に相当します。

$ sudo RKT_EXPERIMENT_APP=true rkt app add c77b3f3b coreos.com/etcd:v3.1.7
$ sudo rkt list
UUID        APP     IMAGE NAME                STATE      CREATED         STARTED         NETWORKS
c77b3f3b    etcd    coreos.com/etcd:v3.1.7    running    7 minute ago    7 minute ago    default:ip4=172.16.28.2
$ sudo RKT_EXPERIMENT_APP=true rkt app add c77b3f3b docker://redis:4.0.6
$ sudo rkt list
UUID        APP      IMAGE NAME                                   STATE      CREATED         STARTED         NETWORKS
c77b3f3b    etcd     coreos.com/etcd:v3.1.7                       running    7 minute ago    7 minute ago    default:ip4=172.16.28.2
            redis    registry-1.docker.io/library/redis:4.0.6

逆に特定のコンテナだけ終了させて Pod から外すことも可能です。CRI の StopContainer + RemoveContainer に相当します。

$ sudo RKT_EXPERIMENT_APP=true rkt app rm c77b3f3b --app redis
$ sudo rkt list
UUID        APP     IMAGE NAME                STATE      CREATED           STARTED           NETWORKS
c77b3f3b    etcd    coreos.com/etcd:v3.1.7    running    24 minutes ago    24 minutes ago    default:ip4=172.16.28.2

表面上は全く同じように見えますが、rkt app コマンドで作られた Pod は通常の rkt run で作られた Pod とは別物です。例えば rkt run で作られた Pod に後からコンテナを追加しようとするとエラーになります。

$ sudo rkt run coreos.com/etcd:v3.1.7

(別ターミナルで)
$ sudo rkt list
UUID        APP     IMAGE NAME                STATE      CREATED          STARTED          NETWORKS
5c786d20    etcd    coreos.com/etcd:v3.1.7    running    3 minutes ago    3 minutes ago    default:ip4=172.16.28.2
$ sudo RKT_EXPERIMENT_APP=true rkt app add 5c786d20 docker://redis:4.0.6
add: error adding app to pod: immutable pod

Pod が mutable であるかどうかは rkt cat-manifest を用いて Pod の低レベルな情報を取得することで判断できます。Docker で言えば docker inspect に相当するコマンドです。

$ sudo rkt cat-manifest 5c786d20
...
  "annotations": [
    {
      "name": "coreos.com/rkt/stage1/mutable",
      "value": "false"
    }
  ],
...

rktlet のインストール

さて、kubelet から gRPC を待ち受けて rkt app を実行するのが rktlet の役目です。

Unix ソケットで通信している都合上、各 Node 上で(Kubelet と同じサーバで)rktlet が稼働している必要があります。こちらもバイナリがリリースされているのでダウンロードしてくればすぐ使えます。

$ wget https://github.com/kubernetes-incubator/rktlet/releases/download/v0.1.0/rktlet-v0.1.0.tar.gz
$ tar xzvf rktlet-v0.1.0.tar.gz
$ sudo mv rktlet-v0.1.0/rktlet /usr/local/bin/rktlet
$ rktlet --version
rktlet version: v0.1.0

直接フォアグラウンドで実行したまま実験してももちろん構いませんが、もし systemd に登録するのであればテスト用の最低限のサービス定義は以下のようになります。各 Node の /etc/systemd/system/rktlet.service に配置してください。

[Unit]
Description=rktlet: The rkt implementation of a Kubernetes Container Runtime
Documentation=https://github.com/kubernetes-incubator/rktlet/tree/master/docs

[Service]
ExecStart=/usr/local/bin/rktlet
Restart=always
StartLimitInterval=0
RestartSec=10

[Install]
WantedBy=multi-user.target

登録後、サービスとして起動しておきます。

$ sudo systemctl enable rktlet
$ sudo systemctl start rktlet
$ systemctl status rktlet
● rktlet.service - rktlet: The rkt implementation of a Kubernetes Container Runtime
   Loaded: loaded (/etc/systemd/system/rktlet.service; enabled; vendor preset: enabled)
   Active: active (running) since Fri 2017-12-22 17:52:06 UTC; 3s ago
...

Kubelet の設定

さて、次は呼び出す側の設定です。各 Node で稼働している Kubelet の実行時引数に、以下の四つの設定を入れてください。

--cgroup-driver=systemd
--container-runtime=remote
--container-runtime-endpoint=/var/run/rktlet.sock
--image-service-endpoint=/var/run/rktlet.sock

--container-runtime-endpoint CRI の RuntimeService に、--image-service-endpoint が CRI の ImageService に、それぞれ対応していて、Unix ソケット経由で gRPC リクエストが送られるようになっています。

具体的に変更するファイルは Kubernetes をどうやって構築したかに依存するので何とも言えませんが、systemd サービスとして起動させているのであれば /etc/systemd/system/kubelet.service 内の ExecStart もしくは EnvironmentFile に記述があるはずです。

一旦 Kubelet を停止して、稼働している Docker コンテナをすべて止めておくのが安全です。その後、Kubelet を再起動して Node が Ready になるまで待って確認してみると、Node のコンテナランタイム情報は狙い通り rkt に変わっています。

$ kubectl describe nodes/YOUR_NODE_NAME
...
Container Runtime Version:  rkt://0.1.0
...

ただし rkt ではなく rktlet のバージョンが表示されてしまうようです。

実際に立ち上がっている rkt コンテナを確認してみましょう。rktlet はデフォルトの rkt とは別の場所、/var/lib/rktlet/data 以下にコンテナの情報を格納するようになっています。 クラスタ上に何がデプロイされているかによって細かい部分はもちろん違うと思いますが、おおむね以下のように Pod の一覧が表示されるはずです。

$ sudo rkt --dir=/var/lib/rktlet/data list
UUID        APP                       IMAGE NAME                                                              STATE      CREATED          STARTED          NETWORKS
019e15b8    0-kubernetes-dashboard    gcr.io/google_containers/kubernetes-dashboard-amd64:v1.6.3              running    3 minutes ago    3 minutes ago    default:ip4=172.16.28.2
03183628    0-kube-proxy              quay.io/coreos/hyperkube:v1.8.0_coreos.0                                running    3 minutes ago    3 minutes ago 
37bf1575    0-nginx-proxy             registry-1.docker.io/library/nginx:1.11.4-alpine                        running    3 minutes ago    3 minutes ago 
7996fe61    0-autoscaler              gcr.io/google_containers/cluster-proportional-autoscaler-amd64:1.1.1    running    3 minutes ago    3 minutes ago    default:ip4=172.16.28.5
abf84ed7    0-calico-node             quay.io/calico/node:v2.5.0                                              running    3 minutes ago    3 minutes ago
            1-calico-node             quay.io/calico/node:v2.5.0
bf67a103    0-kubedns                 gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.2                  running    3 minutes ago    3 minutes ago    default:ip4=172.16.28.4
            0-dnsmasq                 gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.2
            0-sidecar                 gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.2

この後は通常通り、Deployment やその他のリソースを作成すればマニフェストの記述に従って rkt のコンテナが立ち上がるはずです。

まとめ

今回の記事では、CoreOS 社が作ったシンプル指向のコンテナ技術 rkt の仕組み、および rktlet を用いた Kubernetes との連携について実際のコマンドを交えつつ説明しました。

  • rkt は Docker とは別の Pod ネイティブな仕組みを採用した
  • しかしそれが裏目に出て、CRI への対応が難しくなった
  • kubelet と rkt コマンドの間に rktlet を置き、新しく実装された app サブコマンドを叩かせることで CRI 相当の動作を実現する

ところで、今回の記事では rkt の内部アーキテクチャにはそれほど深く踏み込めませんでした。従来の immutable な Pod と新しい mutable な Pod、それぞれがどうやって systemd 上に実装されているか、というのも興味深い話題ではあるのですが、それはまた別の話。

以上、Kubernetes Advent Calendar 2017 の 23 日目の記事でした。明日はクリスマスイブですね。担当は tkusumi さんです。