kind (Kuberenetes in Docker) に deep dive してみる

kuKubernetes の Install Tools というページでは、kubernetes を local で動かすための tool として kind が紹介されています。今日はこの kind について内部構造及び使い方を見てみます。

kind とは

kind は「Docker container の中で Kubernetes を動かすことが出来るツール」です。kind という命名は「Kubernetes in Docker」から来ていて、K-in-D という頭文字をとったものになっています。

kind については KubeCon + CloudNativeCon で何度か紹介されているようです。例えば KubeCon + CloudNativeCon North America 2019 における以下の "Deep Dive: Kind" というトーク では、「kind とは何か?」について内部実装など含めて紹介されています。

www.youtube.com

上記のトークについて簡単に summary を書くと、kind は以下のようなものとして紹介されています。

  • Docker container として Node(をシミュレートする container)を動かし、その中で Kubernetes を動かすツール
    • Node image の中に、Kubernetes を動かすために必要な全てを詰める
      • kubelet
      • kubeadm
      • docker
      • systemd
      • core images (Kuberentes にとって重要な container image)
        • etcd
        • coredns
        • pause
        • kube-apiserver
        • etc.
  • multi-node 構成も可能
  • Kubernetes を source code から build して動かすことが可能
  • 30s 程度で Kuberenetes cluster を作ることが出来る
  • kind は「Kuberentes 自体の test」のために作られた
    • Kuberenetes の CI は Kubernetes で動いており、全ての test は Pod の中で実行される。そのため、Kuberenetes 自体を test するには、「Kuberentes を container の中で動かす」必要があった。

上記のトークの中で出てくる以下のスライドが、kind の仕組みを端的に示していて分かりやすいと思います。"Node" Container の中で、kubelet や containerd, そして containered を通じて起動された「Kuberentes の動作を支える各種 container」が動いている事が分かります。

f:id:south37:20201230223917p:plain

ここまでで、「kind の内部構造」を説明しました。次に、実際に kind を動かしてみましょう。

kind を動かしてみる

kind の利用方法は実はとても簡単で、README で以下のように言及されている通り「GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster を実行するだけ」となっています。

If you have go (1.11+) and docker installed GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster is all you need!

cf. https://github.com/kubernetes-sigs/kind#please-see-our-documentation-for-more-in-depth-installation-etc

実際に実行すると、以下のように Kubernetes cluster 作成が進みます。自分の環境では、Kuberentes cluster 作成にかかる時間は 1min 強でした。

(なお、Ensuring node image という step で少し時間がかかりますが、これは後述する「kind が利用する kindest/node という docker image の pull に時間がかかっている」だけです。一度 image pull が完了すると次からは 30s 程度で高速に Kubernetes cluster が作成出来る様になります)。

$ GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0 && kind create cluster
go: downloading sigs.k8s.io/kind v0.9.0
go: downloading github.com/spf13/cobra v1.0.0
go: downloading k8s.io/apimachinery v0.18.8
go: downloading gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
go: downloading github.com/mattn/go-isatty v0.0.12
go: downloading github.com/alessio/shellescape v1.2.2
go: downloading sigs.k8s.io/yaml v1.2.0
go: downloading github.com/pelletier/go-toml v1.8.0
go: downloading github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b
go: downloading golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed
go: downloading github.com/evanphx/json-patch/v5 v5.1.0
GO111MODULE="on" go get sigs.k8s.io/kind@v0.9.0  8.93s user 3.64s system 102% cpu 12.228 total

Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kind create cluster  4.95s user 2.65s system 9% cpu 1:16.71 total

これで既に「Kubernetes cluster が Docker container の中で動いている状態」となっています。簡単ですね!

kind で動く Kubernetes cluster を利用してみる

次に、実際に kind で動く Kuberentes cluster を利用してみましょう。

$ kind create cluster を実行した際の message に Set kubectl context to "kind-kind" と出ていたように、既に kubectl の context は「kind で作成した Kubenetes cluster(= kind-kind という名前の cluster)」に切り替わっています。つまり、この状態で $ kubectl を利用すれば、kind-kind cluster の API server に対して通信が行われるようになっています。

実際、以下のように $ kubectl config current-context$ kubectl cluster-info の結果を見てみると「local で動く kind-kind cluster に context が切り替わっている」事が確認出来ます。

$ kubectl config current-context
kind-kind

$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:55999
KubeDNS is running at https://127.0.0.1:55999/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl で cluster 内の k8s object を見てみましょう。例えば namespace や pod, service を見てみると、以下のような内容になっています。

$ kubectl get namespaces
NAME                 STATUS   AGE
default              Active   11m
kube-node-lease      Active   11m
kube-public          Active   11m
kube-system          Active   11m
local-path-storage   Active   11m

$ kubectl get po --all-namespaces
NAMESPACE            NAME                                         READY   STATUS    RESTARTS   AGE
kube-system          coredns-f9fd979d6-9cgbl                      1/1     Running   0          11m
kube-system          coredns-f9fd979d6-wlnmw                      1/1     Running   0          11m
kube-system          etcd-kind-control-plane                      1/1     Running   0          11m
kube-system          kindnet-phgz9                                1/1     Running   0          11m
kube-system          kube-apiserver-kind-control-plane            1/1     Running   0          11m
kube-system          kube-controller-manager-kind-control-plane   1/1     Running   0          11m
kube-system          kube-proxy-dxx9q                             1/1     Running   0          11m
kube-system          kube-scheduler-kind-control-plane            1/1     Running   0          11m
local-path-storage   local-path-provisioner-78776bfc44-66wln      1/1     Running   0          11m

$ kubectl get svc --all-namespaces
NAMESPACE     NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  18m
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   18m

さらに、適当に deployment と service の追加もしてみましょう。自分が昔作った https://github.com/south37/dumper という「request header を print するだけの Docker container」を動かしてみます。

まず、deployment と service の manifest file を用意します。

# dumper-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: dumper
  name: dumper
  namespace: default
spec:
  selector:
    matchLabels:
      app: dumper
  template:
    metadata:
      annotations:
      labels:
        app: dumper
      name: dumper
    spec:
      containers:
      - image: south37/dumper
        livenessProbe:
          httpGet:
            path: /ping
            port: 8080
        name: dumper
        ports:
        - containerPort: 8080
          name: http
        readinessProbe:
          httpGet:
            path: /ping
            port: 8080
# dumper-service.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: dumper
  name: dumper
  namespace: default
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: dumper
  type: ClusterIP

次に、これらの manfest file を apply します。

$ kubectl apply -f dumper-deployment.yaml
deployment.apps/dumper created

$ kubectl apply -f dumper-service.yaml
service/dumper created

apply したものがちゃんと作られている事を確認します。

$ kubectl get all -n default
NAME                          READY   STATUS    RESTARTS   AGE
pod/dumper-6465654fdc-qn729   1/1     Running   0          118s

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/dumper       ClusterIP   10.110.60.42   <none>        80/TCP    114s
service/kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   21m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dumper   1/1     1            1           118s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/dumper-6465654fdc   1         1         1       118s

まず、pod の log を見てみます。すると、healthcheck 用の GET /ping の reqeust が来ている事が確認できます。

$ kubectl logs dumper-6465654fdc-qn729
.
.
.
2020/12/30 12:09:55 GET /ping HTTP/1.1
2020/12/30 12:09:55 Host: 10.244.0.5:8080
2020/12/30 12:09:55 User-Agent: kube-probe/1.19
2020/12/30 12:09:55 Accept-Encoding: gzip
2020/12/30 12:09:55 Connection: close

次に、Kuberentes cluster の中から Service を通した request をしてみましょう。 curl が入っている docker image として radial/busyboxplus:curl を使うことにします(注: radial/busyboxplus:curlhttps://kubernetes.io/docs/concepts/services-networking/connect-applications-service/ の中でも利用されてるので、安全な image だと判断して利用してます)。

すると、以下のように Kubernetes cluster 内の pod から $ curl http://dumper.default で 「default namespace の dumper service へ HTTP request」をして、ちゃんと response が返る事を確認出来ました!

$ kubectl run --rm -it busybox --image=radial/busyboxplus:curl
If you don't see a command prompt, try pressing enter.
[ root@busybox:/ ]$ curl http://dumper.default
Hello, dumper!

pod の log を見てみると、ちゃんと上記 request が pod に到達していることも確認できます。

$ kubectl logs dumper-6465654fdc-qn729
.
.
.
2020/12/30 12:12:00 GET / HTTP/1.1
2020/12/30 12:12:00 Host: dumper.default
2020/12/30 12:12:00 User-Agent: curl/7.35.0
2020/12/30 12:12:00 Accept: */*

簡単にではありますが、Kubernetes cluster としての動作が確認できました。

Docker container として動く Node container の中を見てみる

次に、kind の動作をもう少し深掘りしてみます。具体的には、Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 の中で紹介されていた「Node container の動作」をもう少し見てみます。

まず、Kubernetes cluster を一度削除して作り直すことにします。これは、「作成されたばかりの状態の Kubernetes cluster」で実験するための操作です(特に気にならない人は不要です)。

$ kind delete cluster
Deleting cluster "kind" ...

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
kind create cluster  4.62s user 2.53s system 19% cpu 36.518 total

次に、$ kind create cluster を実行した後の状態で $ docker ps すると、kindest/node:v1.19.1 という image の container が起動している事が分かります。これが、kind が利用する「Node container」のようです。

$ docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED              STATUS          PORTS                       NAMES
45a383679dd2   kindest/node:v1.19.1   "/usr/local/bin/entr…"   About a minute ago   Up 59 seconds   127.0.0.1:56856->6443/tcp   kind-control-plane

$ docker exec して Node container の中をみてみましょう。$ ps fax で process 一覧を見てみると、Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 の中で説明されていた通り、Kubernetes の動作を支える様々な process が起動している事が分かります。containerdkubelet など一部を除くと、そのほかの process は /usr/local/bin/containerd-shim-runc-v2 経由で起動している(= container として起動している)ことも分かります。

$ docker exec -it 45a383679dd2 bash
root@kind-control-plane:/# ps fax
    PID TTY      STAT   TIME COMMAND
   2060 pts/1    Ss     0:00 bash
   2189 pts/1    R+     0:00  \_ ps fax
      1 ?        Ss     0:00 /sbin/init
    124 ?        S<s    0:00 /lib/systemd/systemd-journald
    135 ?        Ssl    0:11 /usr/local/bin/containerd
    310 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 1c426469eb3ae09b744f1c116e6798c65886e218271dfa105fba747b4bfde0d3 -address /run/containerd/containerd.soc
    405 ?        Ss     0:00  \_ /pause
    502 ?        Ssl    0:06  \_ kube-scheduler --authentication-kubeconfig=/etc/kubernetes/scheduler.conf --authorization-kubeconfig=/etc/kubernetes/scheduler.conf --bind-address=127.0.0.1 --ku
    311 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 23d54713b4401fb309725273c78961ea5d7f3a5be157d2a139813b9b9611e220 -address /run/containerd/containerd.soc
    418 ?        Ss     0:00  \_ /pause
    620 ?        Ssl    0:20  \_ etcd --advertise-client-urls=https://172.19.0.2:2379 --cert-file=/etc/kubernetes/pki/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/etcd --initial-a
    318 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id a24a84c14111721de85b657f2bb0b89db44d87e97452ef86690060a0f0fcd3bc -address /run/containerd/containerd.soc
    425 ?        Ss     0:00  \_ /pause
    563 ?        Ssl    1:08  \_ kube-apiserver --advertise-address=172.19.0.2 --allow-privileged=true --authorization-mode=Node,RBAC --client-ca-file=/etc/kubernetes/pki/ca.crt --enable-admissi
    373 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 8a6a56e83a590f4958dbd3262e4b0adbe36acad229ebcb54ef13c657f39c2c0a -address /run/containerd/containerd.soc
    433 ?        Ss     0:00  \_ /pause
    548 ?        Ssl    0:24  \_ kube-controller-manager --allocate-node-cidrs=true --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf --authorization-kubeconfig=/etc/kubernetes
    667 ?        Ssl    0:25 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --cont
    797 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 841020b0c947c1a8c2047a542268bf3dd91f8fb6b15cfc0a993dc61c0769e660 -address /run/containerd/containerd.soc
    819 ?        Ss     0:00  \_ /pause
    898 ?        Ssl    0:00  \_ /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=kind-control-plane
    833 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 6171f628b9c0658a1983b198160fe76b53eeb4118aa8a0c71491ff5516453268 -address /run/containerd/containerd.soc
    864 ?        Ss     0:00  \_ /pause
    948 ?        Ssl    0:00  \_ /bin/kindnetd
   1110 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 1fd32fb832a5e3782d1f39b26b99a30de8872a0a69600a77db3631f274eeb819 -address /run/containerd/containerd.soc
   1153 ?        Ss     0:00  \_ /pause
   1226 ?        Ssl    0:03  \_ /coredns -conf /etc/coredns/Corefile
   1112 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 725b634f7fc5d93ebfb1e63afb8aabcd936e6297ed7ab552e314e1cc90efeec5 -address /run/containerd/containerd.soc
   1160 ?        Ss     0:00  \_ /pause
   1229 ?        Ssl    0:01  \_ local-path-provisioner --debug start --helper-image k8s.gcr.io/build-image/debian-base:v2.1.0 --config /etc/config/config.json
   1350 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 9d07db434b356d9fddb2ae31715adc1209406a604b7068560f379832f5d79ba2 -address /run/containerd/containerd.soc
   1373 ?        Ss     0:00  \_ /pause
   1404 ?        Ssl    0:03  \_ /coredns -conf /etc/coredns/Corefile

containerd で起動している container は、containerd の CLI tool である ctr で管理する事が出来るはずです。少し見てみましょう。

まず、namespace 一覧を見てみると k8s.io という名前の namespace が見つかります(注: この namespace は kuberentes の namespace とは無関係で、「containerd が container を管理する際に利用する namespace 機能」のはずです)。

root@kind-control-plane:/# ctr namespaces ls
NAME   LABELS
k8s.io

次に、この k8s.io namespace 内の container 一覧を $ ctr --namespace k8s.io containers ls で見てみると、予想通り「Kuberentes cluster の動作に利用される container 一覧」をみる事が出来ました。

root@kind-control-plane:/# ctr --namespace k8s.io containers ls
CONTAINER                                                           IMAGE                                                                      RUNTIME
0e7fc11f71638b86f8fd41f046101ebcb16b48976f06826add2d35df4e2ccc10    k8s.gcr.io/kube-controller-manager:v1.19.1                                 io.containerd.runc.v2
114d8f7f34ebc00c00236d7a111961193a6fa300dc90a4114385134f9eeda412    k8s.gcr.io/kube-proxy:v1.19.1                                              io.containerd.runc.v2
1c426469eb3ae09b744f1c116e6798c65886e218271dfa105fba747b4bfde0d3    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
1fd32fb832a5e3782d1f39b26b99a30de8872a0a69600a77db3631f274eeb819    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
2299e2a7b2b7afbb0789b30a4d7f4e57220a650f7368c04439e91c72e5049356    k8s.gcr.io/kube-apiserver:v1.19.1                                          io.containerd.runc.v2
23d54713b4401fb309725273c78961ea5d7f3a5be157d2a139813b9b9611e220    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
35ab22ae753e043553188810bddfec43aa048d8c93f1ca38cf868ee31dbe06fc    k8s.gcr.io/coredns:1.7.0                                                   io.containerd.runc.v2
6171f628b9c0658a1983b198160fe76b53eeb4118aa8a0c71491ff5516453268    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
6212a3ea51397a8491a8defb188faa1c9afb4b678a8fa102ab15ba8c78f98aa2    sha256:0369cf4303ffdb467dc219990960a9baa8512a54b0ad9283eaf55bd6c0adb934    io.containerd.runc.v2
624a9cf197014dcdbf0be5ddd68995566d14ceab00c8ca18fd51eb35cfe999cb    k8s.gcr.io/kube-scheduler:v1.19.1                                          io.containerd.runc.v2
62d494fe1a94a396494ecd30cfa8538db2e1d2055fedac216d19fd21332d3841    sha256:b77790820d01598b2c56f823fa489e3f56be2cb5d6f7dd9eecd68a1995b89c13    io.containerd.runc.v2
725b634f7fc5d93ebfb1e63afb8aabcd936e6297ed7ab552e314e1cc90efeec5    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
841020b0c947c1a8c2047a542268bf3dd91f8fb6b15cfc0a993dc61c0769e660    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
8a6a56e83a590f4958dbd3262e4b0adbe36acad229ebcb54ef13c657f39c2c0a    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
9d07db434b356d9fddb2ae31715adc1209406a604b7068560f379832f5d79ba2    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
a24a84c14111721de85b657f2bb0b89db44d87e97452ef86690060a0f0fcd3bc    k8s.gcr.io/pause:3.3                                                       io.containerd.runc.v2
b7775d2582ea9fdd481cf308d9c8bafa28fffbdaa2c8c0bad3377a9254876a59    k8s.gcr.io/coredns:1.7.0                                                   io.containerd.runc.v2
ecbad3d6f6f5321e46b0d3ac395cb25227b42cfc04b1cec5a2b659fe45fab6cc    sha256:e422121c9c5f97623245b7e600eeb5e223ee623f21fa04da985ae71057d8d70b    io.containerd.runc.v2

このように、「Node container の中で containerd を動かし、その containerd 経由で Kuberentes cluster に必要な container を動かす」という形で kind は動作するようです。Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 の中で説明されていた事ではありますが、改めてその動作を確認できました。

kindminikube の使い分けについて

さて、ここまでは kind というツールの機能について説明してきました。一方で、「local で Kubernetes を動かすツール」としては他に minikube も存在します。これらの使い分けはどうするのが良いでしょうか?

kind vs minikube で検索すると、この2つの使い分けについて言及している記事がいくつか見つかります。例えば https://brennerm.github.io/posts/minikube-vs-kind-vs-k3s.html という記事では、以下のようにそれぞれの特徴が述べられています。

  • minikube
    1. VMKubernetes を起動する
    2. $ minikube dashboard 機能や minikube の addon system が有用
  • kind
    1. Docker container で Kubernetes を起動する
    2. $ kind load docker-image <my-custom-image>:<tag> を実行する事で local で build した image を container registry へ push する事なく Kubernetes cluster から利用する事が可能

kind について言えば、$ kind load docker-image の機能はかなり強力で、例えば custom controller のような「Kubernetes の中で動かしながら開発を進めたいもの」においては極めて有用です。実際、こういった側面があるためか、kubebuilder の Tutorial の Quick Start では「local で Kuberentes cluster を動かす選択肢」として kind が紹介されていたりします。

You’ll need a Kubernetes cluster to run against. You can use KIND to get a local cluster for testing, or run against a remote cluster.

cf. https://book.kubebuilder.io/quick-start.html#test-it-out

また、「VM よりも Docker container の方が扱いに慣れてる」などのケースでも kind の利用にはメリットがありそうだと個人的には感じました。

まとめ

kind の内部構造や使い方についてざっと眺めてみました。

「Kuberentes を Docker container の中で動かす」という言葉だけを聞くと突拍子も無いアイディアに聞こえますが、kubeletcontainerd, その他の Kubernetes cluster に必要な component を "Node" image の中にまとめて配布してると思えば確かに自然ですし、実際にちゃんと動作することも確認できました。

Kuberentes の CI で使われている限り、これからも継続してメンテナンスされていきそうなのも良い点です。「30s で Kubernetes cluster を起動して気軽に動かせるツール」として、とても有用なものだと言えそうです。

なお、今回は「kind の Node container で動く container 一覧を眺めた」だけで、1つ1つの container の動きについては特に言及しませんでした。そのほとんどは「Kubernetes cluster に共通で必要な component」のはずですが、kindnetd は kind におけるデフォルトの CNI plugin である kindnetdaemon だそうです。

Deep Dive: Kind - KubeCon + CloudNativeCon North America 2019 ではこの kindnet を含めて、このブログで言及してない様々な事を説明してるので、さらに詳細が気になる方はぜひ動画の方もみてみてください。また、自分も一部しか読んでませんが、kind の document (https://kind.sigs.k8s.io/) も理解を深める上でとても有用だと思います。

参考情報

おまけ: 既に node image を pull 済の場合に Kuberentes cluster 作成にかかる時間について

最初に kind を動かしてみた際に、「kindest/node の docker pull 部分で時間がかかる」と書きました。そこで、「既に node image を pull 済みの場合」についても、試しに計測してみましょう。

まず、先ほど作成した cluster を削除します。

$ kind delete cluster
Deleting cluster "kind" ...

この状態でも、kindest/node docker image は残っている事が確認できます。

$ docker images | grep kindest
kindest/node                      <none>               37ddbc9063d2   3 months ago    1.33GB

次に、この状態で再度 $ kind create cluster を実行してみます。

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a nice day! 👋
kind create cluster  4.53s user 2.37s system 21% cpu 32.535 total

今回は、上記のように 30s 程度で Kubernetes cluster が起動することを確認できました!🎉 Node image の pull が無ければ、Kuberentes cluster をとても素早く作成出来る事が分かりますね!

おまけ 2: deployment を apply した状態で Node container の中を見てみる

deployment を apply した状態で、Node container の中を見てみましょう。

$ kubectl apply -f dumper-deployment.yaml
deployment.apps/dumper created

この状態だと、(当然ではありますが)apply した内容の pod に相当する container が起動している様子を確認できます。containerd の動作を感じる事が出来て、とても良いですね!

$ docker exec -it 7ff1d05ef709 bash

root@kind-control-plane:/# ps fax
    PID TTY      STAT   TIME COMMAND
.
.
.
   1729 ?        Sl     0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 34794c98df201080b5b22bcef08805e03748023c35f0deec77f962ff301a6835 -address /run/containerd/containerd.sock
   1752 ?        Ss     0:00  \_ /pause
   1879 ?        Ssl    0:00  \_ /app/dumper

root@kind-control-plane:/# ctr --namespace k8s.io images ls | grep dumper
docker.io/south37/dumper:latest                                                                  application/vnd.docker.distribution.manifest.v2+json sha256:5efcf15fbd3439b2c2fff2415957933b45b9531401526c026c41219aed15701c 290.0 MiB linux/amd64 io.cri-containerd.image=managed
docker.io/south37/dumper@sha256:5efcf15fbd3439b2c2fff2415957933b45b9531401526c026c41219aed15701c application/vnd.docker.distribution.manifest.v2+json sha256:5efcf15fbd3439b2c2fff2415957933b45b9531401526c026c41219aed15701c 290.0 MiB linux/amd64 io.cri-containerd.image=managed

root@kind-control-plane:/# ctr --namespace k8s.io containers ls | grep dumper
ab80cafc653433c2b74713b85679797b4ffad5ae54eed733bb40d41af7bb9f43    docker.io/south37/dumper:latest                                            io.containerd.runc.v2

おまけ 3: $ kind load docker-image の実装について

kind の強力な機能である $ kind load docker-image 機能がどう実現されているのか気になったので、少しコードを読んでみました(対象は kind の v0.9.0 tag のコードです)。

まず、$ kind load コマンド自体は https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/load.go で実装されていて、その中の $ kind load docker-image サブコマンドは https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/docker-image/docker-image.go で実装されてるようです。さらにコードを読み進めると、nodeutils package の LoadImageArchive という関数にたどり着きます。

import (
    ...
    dockerimage "sigs.k8s.io/kind/pkg/cmd/kind/load/docker-image"
    ...
)

// NewCommand returns a new cobra.Command for get
func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command {
    cmd := &cobra.Command{
        Args:  cobra.NoArgs,
        Use:   "load",
        Short: "Loads images into nodes",
        Long:  "Loads images into node from an archive or image on host",
    }
    // add subcommands
    cmd.AddCommand(dockerimage.NewCommand(logger, streams))
    cmd.AddCommand(imagearchive.NewCommand(logger, streams))
    return cmd
}

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/load.go#L38

   // Load the image on the selected nodes
    fns := []func() error{}
    for _, selectedNode := range selectedNodes {
        selectedNode := selectedNode // capture loop variable
        fns = append(fns, func() error {
            return loadImage(imageTarPath, selectedNode)
        })
    }

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/docker-image/docker-image.go#L149-L156

// loads an image tarball onto a node
func loadImage(imageTarName string, node nodes.Node) error {
    f, err := os.Open(imageTarName)
    if err != nil {
        return errors.Wrap(err, "failed to open image")
    }
    defer f.Close()
    return nodeutils.LoadImageArchive(node, f)
}

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cmd/kind/load/docker-image/docker-image.go#L162-L170

nodeutils.LoadImageArchive は以下のような実装になっていて、「Node container の中で $ ctr コマンドを呼び出して、container image の load を行う」ようです。

// LoadImageArchive loads image onto the node, where image is a Reader over an image archive
func LoadImageArchive(n nodes.Node, image io.Reader) error {
    cmd := n.Command("ctr", "--namespace=k8s.io", "images", "import", "-").SetStdin(image)
    if err := cmd.Run(); err != nil {
        return errors.Wrap(err, "failed to load image")
    }
    return nil
}

cf. https://github.com/kubernetes-sigs/kind/blob/v0.9.0/pkg/cluster/nodeutils/util.go#L77-L84

nodeutils.LoadImageArchive は「1 つ 1 つの Node container に対して loop を回して実行してる」ようです。つまり、魔法のように見えた $ kind load docker-image の機能は、「それぞれの Node container の中で $ ctr images import を実行して、container image を import する」事で実現しているようです。面白いですね!

BigQuery の内部実装の変遷について

BigQuery(正確にはそのクエリエンジンである Dremel)の内部実装の変遷をまとめた以下のブログポストおよび論文を読みました。

とても面白い内容で、Twitterメモをポストしたのですが、後で参照しやすいようにブログにも同じ内容を載せておきます。

手を動かして学ぶコンテナ標準 - Container Runtime 編

コンテナ標準化の現状と Kubernetes の立ち位置について というブログではコンテナ標準の現状についてまとめてみました。

また、手を動かして学ぶコンテナ標準 - Container Image と Container Registry 編 というブログでは Container Image と Container Registry について手を動かして学んでみました。

このブログでは、runc, containerd などの Container Runtime について、実際に手を動かして学んでみたいと思います。

なお、前回のブログ 同様、基本的にこのブログ内のコマンドは Linux で実行するものとします(自分は MacOSVagrantUbuntu VM を立てて実験してます)。

runc を動かしてみる

runc は Low-Level Container Runtime と呼ばれるもので、OCI Runtime Specification に準拠した CLI tool となっています。実際に runc を動かしてみることで、Container Runtime に対して理解を深めてみましょう。

まず runc の install ですが、自分が試している ubuntu-20.04 では apt で install することができます。

vagrant@vagrant:~$ sudo apt update -y
vagrant@vagrant:~$ sudo apt install -y runc
vagrant@vagrant:~$ which runc
/usr/sbin/runc

help を見てみると以下のような内容になっていて、container を操作するために必要な各種 subcommand が存在することがわかります。実は、これらの subcommand によって OCI Runtime SpecificationRuntime and Lifecycle で定義された「Operations」 の機能が提供されています。ただし、見比べてみると分かりますが runc 自体はよりリッチな機能を提供しているようです。

vagrant@vagrant:~$ runc --help
NAME:
   runc - Open Container Initiative runtime

runc is a command line client for running applications packaged according to
the Open Container Initiative (OCI) format and is a compliant implementation of the
Open Container Initiative specification.

runc integrates well with existing process supervisors to provide a production
container runtime environment for applications. It can be used with your
existing process monitoring tools and the container will be spawned as a
direct child of the process supervisor.

Containers are configured using bundles. A bundle for a container is a directory
that includes a specification file named "config.json" and a root filesystem.
The root filesystem contains the contents of the container.

To start a new instance of a container:

    # runc run [ -b bundle ] <container-id>

Where "<container-id>" is your name for the instance of the container that you
are starting. The name you provide for the container instance must be unique on
your host. Providing the bundle directory using "-b" is optional. The default
value for "bundle" is the current directory.

USAGE:
   runc [global options] command [command options] [arguments...]

VERSION:
   spec: 1.0.1-dev

COMMANDS:
     checkpoint  checkpoint a running container
     create      create a container
     delete      delete any resources held by the container often used with detached container
     events      display container events such as OOM notifications, cpu, memory, and IO usage statistics
     exec        execute new process inside the container
     init        initialize the namespaces and launch the process (do not call it outside of runc)
     kill        kill sends the specified signal (default: SIGTERM) to the container's init process
     list        lists containers started by runc with the given root
     pause       pause suspends all processes inside the container
     ps          ps displays the processes running inside a container
     restore     restore a container from a previous checkpoint
     resume      resumes all processes that have been previously paused
     run         create and run a container
     spec        create a new specification file
     start       executes the user defined process in a created container
     state       output the state of a container
     update      update container resource constraints
     help, h     Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --debug             enable debug output for logging
   --log value         set the log file path where internal debug information is written
   --log-format value  set the format used by logs ('text' (default), or 'json') (default: "text")
   --root value        root directory for storage of container state (this should be located in tmpfs) (default: "/run/user/1000/runc")
   --criu value        path to the criu binary used for checkpoint and restore (default: "criu")
   --systemd-cgroup    enable systemd cgroup support, expects cgroupsPath to be of form "slice:prefix:name" for e.g. "system.slice:runc:434234"
   --rootless value    ignore cgroup permission errors ('true', 'false', or 'auto') (default: "auto")
   --help, -h          show help
   --version, -v       print the version

実際に runc を利用して container を起動してみましょう。runc の README の Using runc というセクションがぴったりの題材なので、これを参考にしてみます。

OCI Runtime Speicification の内容を見てもらうと分かるのですが、runc のような Low-Level Container Runtime は Filesystem Bundle と呼ばれる「Container を起動するのに利用される configuration file と、container にとっての root filesystem となる directory の組み合わせ」を必要とします。まずはこの Filesystem Bundle を用意します。

Filesystem Bundle は config.json と呼ばれる configuration file と、その config.json の中の root.path で path を指定される 「container の root filesystem となる directory」から構成されます。それぞれ順番に作成していきます。

まずは Filesystem Bundle を格納する directory として mycontainer という directory を作っておきます。

vagrant@vagrant:~$ mkdir mycontainer
vagrant@vagrant:~$ cd mycontainer/

次に、config.json についてですが、 $ runc spec を実行すると自動でデフォルト設定の config.json が作成されます。今回はこれを使うことにしましょう。

vagrant@vagrant:~/mycontainer$ runc spec
vagrant@vagrant:~/mycontainer$ ls
config.json

デフォルト設定の config.json は以下のような内容になっています。実行時のコマンドや環境変数、root.path などに加えて、namespace や volume mount, capability の設定なども記載されています。

vagrant@vagrant:~/mycontainer$ cat config.json
{
        "ociVersion": "1.0.1-dev",
        "process": {
                "terminal": true,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sh"
                ],
                "env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "TERM=xterm"
                ],
                "cwd": "/",
                "capabilities": {
                        "bounding": [
                                "CAP_AUDIT_WRITE",
                                "CAP_KILL",
                                "CAP_NET_BIND_SERVICE"
                        ],
                        "effective": [
                                "CAP_AUDIT_WRITE",
                                "CAP_KILL",
                                "CAP_NET_BIND_SERVICE"
                        ],
                        "inheritable": [
                                "CAP_AUDIT_WRITE",
                                "CAP_KILL",
                                "CAP_NET_BIND_SERVICE"
                        ],
                        "permitted": [
                                "CAP_AUDIT_WRITE",
                                "CAP_KILL",
                                "CAP_NET_BIND_SERVICE"
                        ],
                        "ambient": [
                                "CAP_AUDIT_WRITE",
                                "CAP_KILL",
                                "CAP_NET_BIND_SERVICE"
                        ]
                },
                "rlimits": [
                        {
                                "type": "RLIMIT_NOFILE",
                                "hard": 1024,
                                "soft": 1024
                        }
                ],
                "noNewPrivileges": true
        },
        "root": {
                "path": "rootfs",
                "readonly": true
        },
        "hostname": "runc",
        "mounts": [
                {
                        "destination": "/proc",
                        "type": "proc",
                        "source": "proc"
                },
                {
                        "destination": "/dev",
                        "type": "tmpfs",
                        "source": "tmpfs",
                        "options": [
                                "nosuid",
                                "strictatime",
                                "mode=755",
                                "size=65536k"
                        ]
                },
                {
                        "destination": "/dev/pts",
                        "type": "devpts",
                        "source": "devpts",
                        "options": [
                                "nosuid",
                                "noexec",
                                "newinstance",
                                "ptmxmode=0666",
                                "mode=0620",
                                "gid=5"
                        ]
                },
                {
                        "destination": "/dev/shm",
                        "type": "tmpfs",
                        "source": "shm",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev",
                                "mode=1777",
                                "size=65536k"
                        ]
                },
                {
                        "destination": "/dev/mqueue",
                        "type": "mqueue",
                        "source": "mqueue",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev"
                        ]
                },
                {
                        "destination": "/sys",
                        "type": "sysfs",
                        "source": "sysfs",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev",
                                "ro"
                        ]
                },
                {
                        "destination": "/sys/fs/cgroup",
                        "type": "cgroup",
                        "source": "cgroup",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev",
                                "relatime",
                                "ro"
                        ]
                }
        ],
        "linux": {
                "resources": {
                        "devices": [
                                {
                                        "allow": false,
                                        "access": "rwm"
                                }
                        ]
                },
                "namespaces": [
                        {
                                "type": "pid"
                        },
                        {
                                "type": "network"
                        },
                        {
                                "type": "ipc"
                        },
                        {
                                "type": "uts"
                        },
                        {
                                "type": "mount"
                        }
                ],
                "maskedPaths": [
                        "/proc/acpi",
                        "/proc/asound",
                        "/proc/kcore",
                        "/proc/keys",
                        "/proc/latency_stats",
                        "/proc/timer_list",
                        "/proc/timer_stats",
                        "/proc/sched_debug",
                        "/sys/firmware",
                        "/proc/scsi"
                ],
                "readonlyPaths": [
                        "/proc/bus",
                        "/proc/fs",
                        "/proc/irq",
                        "/proc/sys",
                        "/proc/sysrq-trigger"
                ]
        }

次に、container の root filesystem となる directory を用意します。今回は楽をするために、「$ docker export の機能を利用する」ことにします(自分は vagrant で synced_folder した directory で docker export しました)。原理的には、どんな手段で用意しても良いはずです。

$ mkdir rootfs
$ docker export $(docker create busybox) | tar -C rootfs -xvf -

上記コマンドまで実行が終わると、mycontainer directory の中は以下のような状態になっているはずです。

vagrant@vagrant:~/mycontainer$ ls
config.json  rootfs

この状態で $ sudo runc run containerid を実行すると、container が起動します。

vagrant@vagrant:~/oci-playground/runc-playground/mycontainer2$ sudo runc run containerid
/ #

上記で作成した config.json はデフォルトで「sh コマンドを実行する」という設定になっているので、sh が起動しています。

そのため、一度 exit してから config.json を例えば「ls を実行するもの」に書き換えると、ls が container 内で実行されます。

vagrant@vagrant:~/oci-playground/runc-playground/mycontainer2$ vim config.json  # ここで以下のように変更
                "args": [
-                       "sh"
+                       "ls"
                ],

vagrant@vagrant:~/oci-playground/runc-playground/mycontainer2$ sudo runc run containerid
bin   dev   etc   home  proc  root  sys   tmp   usr   var

ということで、runc を利用して無事に container を起動することができました!

config.json + container の root filesystem になる directory という「Filesystem Bundle」を用意さえすれば、「container の起動が簡単に出来る」ということが分かったかと思います。なお、これは OCI Runtime Specification で定められている挙動であるため、この仕様を満たしてさえいれば「runc 以外の Low-Level Container Runtime」においても同様のことができるはずです。

上記の例では runc run を利用しましたが、その他のオペレーション(create, start, delete など)を利用することでより細かな制御も可能です。詳細が気になる方は、ぜひご自分でも試してみてください。

containerd を動かしてみる

Low-Level Container Runtime の runc の次は、High-Level Container Runtime である containerd を動かしてみましょう。runc は「container を起動する」という部分は実行してくれましたが、それ以外の「Container Image の Pull、Container Image から Filesystem Bundle への変換」などはやってくれませんでした。実は、この部分の機能を提供してくれてるのが containerd です。

containerd は、daemon として常駐して、client からの request を受け付けて動作するという振る舞いになっています。containerd の動作のイメージを掴むために、Getting started というページに従って containerd を利用してみましょう。

まずは、containerd downloads の Installing binaries に従って containerd の binary を download します。そして、その binary を /usr/local/bin など PATH の通った場所に配置します。

vagrant@vagrant:~$ wget https://github.com/containerd/containerd/releases/download/v1.4.3/containerd-1.4.3-linux-amd64.tar.gz
vagrant@vagrant:~$ tar xvf containerd-1.4.3-linux-amd64.tar.gz
vagrant@vagrant:~$ sudo mv bin/* /usr/local/bin/

これで、containerd がコマンドとして利用できるようになります。

vagrant@vagrant:~$ which containerd
/usr/local/bin/containerd

vagrant@vagrant:~$ containerd --help
NAME:
   containerd -
                    __        _                     __
  _________  ____  / /_____ _(_)___  ___  _________/ /
 / ___/ __ \/ __ \/ __/ __ `/ / __ \/ _ \/ ___/ __  /
/ /__/ /_/ / / / / /_/ /_/ / / / / /  __/ /  / /_/ /
\___/\____/_/ /_/\__/\__,_/_/_/ /_/\___/_/   \__,_/

high performance container runtime


USAGE:
   containerd [global options] command [command options] [arguments...]

VERSION:
   v1.4.3

DESCRIPTION:

containerd is a high performance container runtime whose daemon can be started
by using this command. If none of the *config*, *publish*, or *help* commands
are specified, the default action of the **containerd** command is to start the
containerd daemon in the foreground.


A default configuration is used if no TOML configuration is specified or located
at the default file location. The *containerd config* command can be used to
generate the default configuration for containerd. The output of that command
can be used and modified as necessary as a custom configuration.

COMMANDS:
   config    information on the containerd config
   publish   binary to publish events to containerd
   oci-hook  provides a base for OCI runtime hooks to allow arguments to be injected.
   help, h   Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --config value, -c value     path to the configuration file (default: "/etc/containerd/config.toml")
   --log-level value, -l value  set the logging level [trace, debug, info, warn, error, fatal, panic]
   --address value, -a value    address for containerd's GRPC server
   --root value                 containerd root directory
   --state value                containerd state directory
   --help, -h                   show help
   --version, -v                print the version

ただし、前述したように containerd は daemon として動作する必要があるので、これだけではまだ利用できません。

Getting started を見ると、どうやら containerd のソースコードに同梱されている containerd.service に言及しています。これを利用して、systemd で containerd を起動することにしてみましょう。

vagrant@vagrant:~$ sudo apt install -y unzip  # unzip を install しておく

vagrant@vagrant:~$ wget https://github.com/containerd/containerd/archive/v1.4.3.zip  # ソースコードを取得
vagrant@vagrant:~$ unzip v1.4.3.zip
vagrant@vagrant:~$ ls
containerd-1.4.3  v1.4.3.zip

vagrant@vagrant:~$ ls containerd-1.4.3/containerd.service  # systemd 向けの service file が存在
containerd-1.4.3/containerd.service

vagrant@vagrant:~$ sudo cp containerd-1.4.3/containerd.service /etc/systemd/system/
vagrant@vagrant:~$ sudo systemctl enable containerd
Created symlink /etc/systemd/system/multi-user.target.wants/containerd.service → /etc/systemd/system/containerd.service.
vagrant@vagrant:~$ sudo systemctl start containerd

これで、無事 containerd が起動しました。

vagrant@vagrant:~$ ps aux | grep containerd
root       13444  1.6  4.5 1344964 45620 ?       Ssl  16:39   0:00 /usr/local/bin/containerd
vagrant    13455  0.0  0.0   9032   664 pts/0    S+   16:39   0:00 grep --color=auto containerd

Getting started の document には /etc/containerd/config.toml の存在も言及されているので、これも用意しておきます。これで準備 OK です。

vagrant@vagrant:~$ sudo mkdir /etc/containerd
vagrant@vagrant:~$ sudo vim /etc/containerd/config.toml  # ここで containerd の設定ファイルである config.toml を作成
vagrant@vagrant:~$ sudo systemctl restart containerd  # ここで、設定ファイルを読み込む形で containerd を再起動
# これが /etc/containerd/config.toml の内容
subreaper = true
oom_score = -999

[debug]
        level = "debug"

[metrics]
        address = "127.0.0.1:1338"

[plugins.linux]
        runtime = "runc"
        shim_debug = true

さて、containerd が daemon として動くようになりました。次は、containerd と通信するコードを書いてみましょう。Getting started を参考に、以下のような Go コードを書いてみます。

package main

import (
    "context"
    "fmt"
    "log"
    "syscall"
    "time"

    "github.com/containerd/containerd"
    "github.com/containerd/containerd/cio"
    "github.com/containerd/containerd/namespaces"
    "github.com/containerd/containerd/oci"
)

func main() {
    if err := redisExample(); err != nil {
        log.Fatal(err)
    }
}

func redisExample() error {
    // create a new client connected to the default socket path for containerd
    client, err := containerd.New("/run/containerd/containerd.sock")
    if err != nil {
        return err
    }
    defer client.Close()

    // create a new context with an "example" namespace
    ctx := namespaces.WithNamespace(context.Background(), "example")

    // pull the redis image from DockerHub
    image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
    if err != nil {
        return err
    }

    // create a container
    container, err := client.NewContainer(
        ctx,
        "redis-server",
        containerd.WithImage(image),
        containerd.WithNewSnapshot("redis-server-snapshot", image),
        containerd.WithNewSpec(oci.WithImageConfig(image)),
    )
    if err != nil {
        return err
    }
    defer container.Delete(ctx, containerd.WithSnapshotCleanup)

    // create a task from the container
    task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
    if err != nil {
        return err
    }
    defer task.Delete(ctx)

    // make sure we wait before calling start
    exitStatusC, err := task.Wait(ctx)
    if err != nil {
        fmt.Println(err)
    }

    // call start on the task to execute the redis server
    if err := task.Start(ctx); err != nil {
        return err
    }

    // sleep for a lil bit to see the logs
    time.Sleep(3 * time.Second)

    // kill the process and get the exit status
    if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
        return err
    }

    // wait for the process to fully exit and print out the exit status
    status := <-exitStatusC // Block here.
    code, _, err := status.Result()
    if err != nil {
        return err
    }
    fmt.Printf("redis-server exited with status: %d\n", code)

    // For Debug
    fmt.Printf("Client: %v\n", client)
    fmt.Printf("Image: %v\n", image)
    fmt.Printf("Container: %v\n", container)
    fmt.Printf("Task: %v\n", task)

    return nil
}

「Go 環境の構築」は良い感じにやってください。その状態で上記の Go コードを実行すると、以下のような出力が行われます。

vagrant@vagrant:~/containerd-playground$ go build main.go && sudo ./main
1:C 10 Dec 2020 17:11:43.852 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 10 Dec 2020 17:11:43.852 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 10 Dec 2020 17:11:43.852 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 10 Dec 2020 17:11:43.856 # You requested maxclients of 10000 requiring at least 10032 max file descriptors.
1:M 10 Dec 2020 17:11:43.856 # Server can't set maximum open files to 10032 because of OS error: Operation not permitted.
1:M 10 Dec 2020 17:11:43.856 # Current maximum open files is 1024. maxclients has been reduced to 992 to compensate for low ulimit. If you need higher maxclients increase 'ulimit -n'.
1:M 10 Dec 2020 17:11:43.858 * Running mode=standalone, port=6379.
1:M 10 Dec 2020 17:11:43.858 # Server initialized
1:M 10 Dec 2020 17:11:43.858 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
1:M 10 Dec 2020 17:11:43.858 * Ready to accept connections
1:signal-handler (1607620306) Received SIGTERM scheduling shutdown...
1:M 10 Dec 2020 17:11:46.877 # User requested shutdown...
1:M 10 Dec 2020 17:11:46.877 * Saving the final RDB snapshot before exiting.
1:M 10 Dec 2020 17:11:46.879 * DB saved on disk
1:M 10 Dec 2020 17:11:46.879 # Redis is now ready to exit, bye bye...
redis-server exited with status: 0
Client: &{{<nil> <nil> <nil> <nil> map[] <nil> <nil> <nil> <nil> <nil>} {0 0} 0xc00010ee00 io.containerd.runc.v2  {0xc0002131a0} 0xe6c020}
Image: &{0xc0002f4000 {docker.io/library/redis:alpine map[] {application/vnd.docker.distribution.manifest.list.v2+json sha256:b0e84b6b92149194d99953e44f7d1fa1f470a769529bb05b4164eae60d8aea6c 1645 [] map[] <nil>} {312279631 63742833489 <nil>} {749989962 63743217103 <nil>}} {0xc0002131a0}}
Container: &{0xc0002f4000 redis-server {redis-server map[] docker.io/library/redis:alpine {io.containerd.runc.v2 <nil>} 0xc0003ce1e0 redis-server-snapshot overlayfs {775955650 63743217103 <nil>} {775955650 63743217103 <nil>} map[]}}
Task: &{0xc0002f4000 0xc0001020c0 0xc0003a86c0 redis-server 65235}

上記の Go コードおよび出力について少し説明します。

上記の Go コードは「containerd と /run/containerd/containerd.sock を通じて通信する client の動作」が記述されています。コード内では client の作成、Container Image の pull、Container Image からの Snapshot (= Container 実行時に root filesystem として利用するもの) および Spec (= Container 実行時のメタデータ、Filesystem Bundle における config.json に相当) の作成、Task (= container の中で実際に実行したいコマンド) の作成、開始、終了およびその待ち合わせを行っています。 また、最後にデバッグ用途に様々な struct を print してみています(これは Getting started に掲載されていたコードに自分が後から追加したものです)。

Pull している Container Image は docker.io/library/redis:alpine で、ログの出力としても redis server の起動および終了が行われていることが分かります。

これで、containerd を実際に利用してみる事が出来ました!

なお、containerd は OSS でコードが公開されているので、気になった挙動はコードを読んで理解する事が出来ます。例えば、 containerd.WithNewSnapshotcontainerd.WithNewSpec というのは以下のコードに該当します。

https://github.com/containerd/containerd/blob/v1.4.3/container_opts.go#L144-L169 https://github.com/containerd/containerd/blob/v1.4.3/container_opts.go#L243-L253

前者のコードを読み進めると「Snapshotter によって Snapshot 作成を行って、Container には SnapshotKey を設定して id を通して参照できるようにしている」事が分かりますし、後者のコードを読み進めると「OCI Runtime Specification の Go binding で定義された Spec struct が generate されて利用される」事が分かります。「runc などの Low-Level Container Runtime が必要とする情報を用意する」という部分をしっかりやってくれてるようです(注: これは自分がコードを読んだ理解なので、誤解してる可能もあります。何か気がついた際はコメントいただけるとありがたいです)。

その他、Container Image を Pull している部分や Task の管理部分なども興味深い部分です。気になった箇所はぜひ読み進めて理解を深めてみてください。

まとめ

runccontainerd などの Container Runtime を実際に触ってみる事で、理解を深めました。runc については、OCI Runtime Specification で定義された挙動が CLI tool として提供されている事が分かりました。containerd については、Container Image の Pull や Container Image から Filesystem Bundle 相当の情報(= Container 実行に必要な Snapshot および Spec)への変換、Container や Task の作成・削除・開始などの管理を行っている事が分かりました。

runccontainerd は扱っている領域が異なる一方で、どちらも「Container Runtime」という名前で呼ばれるために混乱してしまいがちです。実際に手を動かしてみる事で、「それぞれの責任範囲」や「レイヤー構造」について理解が深まったように思います。

なお、このブログ自体は自分の理解のために試したことをまとめたものですが、誰か他の人にとっても理解を助けるものになっていればとても幸いです。

Vagrant での実験環境

以下のような Vagrantfile を使ってます。ubuntu-20.04 を使ってます。

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "bento/ubuntu-20.04"
  config.vm.synced_folder "./", "/home/vagrant/oci-playground"
end

手を動かして学ぶコンテナ標準 - Container Image と Container Registry 編

先日は、コンテナ標準化の現状と Kubernetes の立ち位置について において、各種ドキュメントをベースにコンテナ標準についてまとめてみました。

このブログでは、実際に tool などに触れて手を動かすことで、コンテナ標準についてさらに理解を深めてみたいと思います。

なお、基本的にこのブログ内のコマンドは、Linux で実行するものとします(自分は MacOSVagrantUbuntu VM を立てて実験してます)。

OCI Image の中身を見てみる

skopeo と呼ばれる「container image に対して様々な操作を行えるツール」があります。このツールを利用することで、「docker image から OCI Image への変換」を行うことができます。このツールを利用して、実際に OCI Image の中身を見てみましょう。

まず、以下のコマンドを実行して ruby:2.7.2-slim という docker image を oci:ruby-oci:2.7.2 という名前の OCI Image に変換します。

vagrant@vagrant:~/oci-playground$ skopeo copy docker://ruby:2.7.2-slim oci:ruby-oci:2.7.2
Getting image source signatures
Copying blob 852e50cd189d done
Copying blob 6de4319615e2 done
Copying blob 150eb06190d1 done
Copying blob cf654ff9d9df done
Copying blob 0a529f6cf42e done
Copying config 3265430f5e done
Writing manifest to image destination
Storing signatures

上記コマンドを実行すると、ruby-oci という directory が出来ています。

vagrant@vagrant:~/oci-playground$ ls
ruby-oci

ruby-oci directory の中を見てみると、以下のように blobs という direcyory と index.json, oci-layout という file が出来ています。これは、OCI Image Format Specification で定められた Image Layout の内容に一致しています。

vagrant@vagrant:~/oci-playground/ruby-oci$ ls
blobs  index.json  oci-layout

oci-layout file には imageLayoutVersion だけが記載されています。現時点では 1.0.0 が記載されているだけなので、将来の拡張のための file と考えると良いでしょう。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat oci-layout | jq .
{
  "imageLayoutVersion": "1.0.0"
}

index.json は OCI Image のエントリーポイントとも呼べる file で、ここには以下のように「manifest fileへの参照(= Image Manifest を指し示す Content Descriptor)」が記載されています。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat index.json | jq .
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7",
      "size": 976,
      "annotations": {
        "org.opencontainers.image.ref.name": "2.7.2"
      }
    }
  ]
}

ここで出てきた「Content Descriptor」というのが OCI Image Format において特徴的なもので、これは「mediaType, digest, size の 3 つ組 + optional な情報 (e.g. annotations)」となっています。 mediaType が参照先の情報の種類、digest が参照先の情報の path、size が参照先の情報のバイト数を表しています。

digest で示されているのは「blobs directory 以下の file path」になっていて、例えば上記の sha256:ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7 という digest は blobs/sha256/ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7 という path を表しています。実際に、file の中身を見てみると以下のような JSON になっています。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat blobs/sha256/ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7 | jq .
{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:3265430f5e5babe0664d7f7bcc77db2ef7d5feaa1625c06c10b1409ad2952133",
    "size": 4598
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:852e50cd189dfeb54d97680d9fa6bed21a6d7d18cfb56d6abfe2de9d7f173795",
      "size": 27105484
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:6de4319615e27e1aaaadc89b43db39ea0e118f47eeecfa4c8b910ca2fd810653",
      "size": 12539406
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:150eb06190d1ba56f7b998da25a140c21258bca436d33e2e77df679d77ab364a",
      "size": 198
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:cf654ff9d9df475122683b6bd070fa57a1e1969ced2a45f2c1f76a0678495ef2",
      "size": 22852677
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:0a529f6cf42e0fb49fe3fb4d12e232b26db923ab85a442563b0a7ae0a28c5971",
      "size": 143
    }
  ]
}

mediaTypeapplication/vnd.oci.image.manifest.v1+json だったことから、これは Image Manifest であると分かります。実際に、Image Manifest の仕様で定義された内容と一致しており、config (Container Image のメタデータ)や layers (Container Image の Layer、Docker Image における Layer Cache の単位となるもの)を情報として持つことも分かります。また、それらの情報への参照も、先ほどと同様の Content Descriptor 形式で表されていることが分かります。

config の内容は、以下のような Image Configuration となっています。環境変数や Command など Container 実行時に必要な各種メタデータや、Container Image 作成時の history の情報が記載されています。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat blobs/sha256/3265430f5e5babe0664d7f7bcc77db2ef7d5feaa1625c06c10b1409ad2952133 | jq .
{
  "created": "2020-11-18T15:35:15.373100656Z",
  "architecture": "amd64",
  "os": "linux",
  "config": {
    "Env": [
      "PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "LANG=C.UTF-8",
      "RUBY_MAJOR=2.7",
      "RUBY_VERSION=2.7.2",
      "RUBY_DOWNLOAD_SHA256=1b95ab193cc8f5b5e59d2686cb3d5dcf1ddf2a86cb6950e0b4bdaae5040ec0d6",
      "GEM_HOME=/usr/local/bundle",
      "BUNDLE_SILENCE_ROOT_WARNING=1",
      "BUNDLE_APP_CONFIG=/usr/local/bundle"
    ],
    "Cmd": [
      "irb"
    ]
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:f5600c6330da7bb112776ba067a32a9c20842d6ecc8ee3289f1a713b644092f8",
      "sha256:70ca8ae918406dce7acc5fe0f49e45b9275a266b83e275922e67358976c2929e",
      "sha256:e8ace463e6f7085a5439cf3b578a080fbefc8ad8424b59b9f35590adb1509763",
      "sha256:71e4ad27368acf7dbb5c90aa65d67cc462267836aa220cbafb9bb62acd9d48de",
      "sha256:1946ed62a3cb062940077a7a1dbfc93d55be6ef3d4f605883b42f71970381662"
    ]
  },
  "history": [
    {
      "created": "2020-11-17T20:21:17.570073346Z",
      "created_by": "/bin/sh -c #(nop) ADD file:d2abb0e4e7ac1773741f51f57d3a0b8ffc7907348842d773f8c341ba17f856d5 in / "
    },
    {
      "created": "2020-11-17T20:21:17.865210281Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"bash\"]",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:21:22.717162717Z",
      "created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tbzip2 \t\tca-certificates \t\tlibffi-dev \t\tlibgmp-dev \t\tlibssl-dev \t\tlibyaml-dev \t\tprocps \t\tzlib1g-dev \t; \trm -rf /var/lib/apt/lists/*"
    },
    {
      "created": "2020-11-18T15:21:23.811888513Z",
      "created_by": "/bin/sh -c set -eux; \tmkdir -p /usr/local/etc; \t{ \t\techo 'install: --no-document'; \t\techo 'update: --no-document'; \t} >> /usr/local/etc/gemrc"
    },
    {
      "created": "2020-11-18T15:21:24.004412503Z",
      "created_by": "/bin/sh -c #(nop)  ENV LANG=C.UTF-8",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:30:41.383881949Z",
      "created_by": "/bin/sh -c #(nop)  ENV RUBY_MAJOR=2.7",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:30:41.629378277Z",
      "created_by": "/bin/sh -c #(nop)  ENV RUBY_VERSION=2.7.2",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:30:41.868222399Z",
      "created_by": "/bin/sh -c #(nop)  ENV RUBY_DOWNLOAD_SHA256=1b95ab193cc8f5b5e59d2686cb3d5dcf1ddf2a86cb6950e0b4bdaae5040ec0d6",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:11.770005784Z",
      "created_by": "/bin/sh -c set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tautoconf \t\tbison \t\tdpkg-dev \t\tgcc \t\tlibbz2-dev \t\tlibgdbm-compat-dev \t\tlibgdbm-dev \t\tlibglib2.0-dev \t\tlibncurses-dev \t\tlibreadline-dev \t\tlibxml2-dev \t\tlibxslt-dev \t\tmake \t\truby \t\twget \t\txz-utils \t; \trm -rf /var/lib/apt/lists/*; \t\twget -O ruby.tar.xz \"https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz\"; \techo \"$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz\" | sha256sum --check --strict; \t\tmkdir -p /usr/src/ruby; \ttar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1; \trm ruby.tar.xz; \t\tcd /usr/src/ruby; \t\t{ \t\techo '#define ENABLE_PATH_CHECK 0'; \t\techo; \t\tcat file.c; \t} > file.c.new; \tmv file.c.new file.c; \t\tautoconf; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--disable-install-doc \t\t--enable-shared \t; \tmake -j \"$(nproc)\"; \tmake install; \t\tapt-mark auto '.*' > /dev/null; \tapt-mark manual $savedAptMark > /dev/null; \tfind /usr/local -type f -executable -not \\( -name '*tkinter*' \\) -exec ldd '{}' ';' \t\t| awk '/=>/ { print $(NF-1) }' \t\t| sort -u \t\t| xargs -r dpkg-query --search \t\t| cut -d: -f1 \t\t| sort -u \t\t| xargs -r apt-mark manual \t; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \t\tcd /; \trm -r /usr/src/ruby; \t! dpkg -l | grep -i ruby; \t[ \"$(command -v ruby)\" = '/usr/local/bin/ruby' ]; \truby --version; \tgem --version; \tbundle --version"
    },
    {
      "created": "2020-11-18T15:35:12.227711802Z",
      "created_by": "/bin/sh -c #(nop)  ENV GEM_HOME=/usr/local/bundle",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:12.563337139Z",
      "created_by": "/bin/sh -c #(nop)  ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP_CONFIG=/usr/local/bundle",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:12.907595531Z",
      "created_by": "/bin/sh -c #(nop)  ENV PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:14.977063521Z",
      "created_by": "/bin/sh -c mkdir -p \"$GEM_HOME\" && chmod 777 \"$GEM_HOME\""
    },
    {
      "created": "2020-11-18T15:35:15.373100656Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"irb\"]",
      "empty_layer": true
    }
  ]
}

layersFilesystem Layer を表しています。tar+gzip という mediaType の suffix は「gzip 圧縮された tar archive」を表しています。試しに、最も root にあった sha256:852e50cd189dfeb54d97680d9fa6bed21a6d7d18cfb56d6abfe2de9d7f173795 の中身を見てみます。

vagrant@vagrant:~/oci-playground/ruby-oci$ mkdir rootfs
vagrant@vagrant:~/oci-playground/ruby-oci$ tar xvzf blobs/sha256/852e50cd189dfeb54d97680d9fa6bed21a6d7d18cfb56d6abfe2de9d7f173795 -C rootfs/
.
.
.

上記コマンドで、rootfs directory 以下に圧縮されていた中身が展開されます(注: tar: bin/uncompress: Cannot hard link to ‘bin/gunzip’: Operation not permitted など一部の file について error は出ていて、そのせいで tar: Exiting with failure status due to previous errors という失敗 message も出てしまいましたが、それはここでは無視します)。

rootfs の中身を見てみると、以下のようにいくつかの directyory が並んでいます。

vagrant@vagrant:~/oci-playground/ruby-oci$ ls rootfs/
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

試しに変換前の ruby:2.7.2-slim docker image を利用して container を起動してみると、root directory の中身がそっくりであることが確認できます。

$ docker run -it ruby:2.7.2-slim bash
root@f6be3c7c619d:/# ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

実は、これらの layer が「どう apply されるのか」は Image Layer Filesystem Changeset の Applying Changesets というセクション の中で以下のように明確に定義されています。ざっくり言えば「layer の上から順に tar archive を展開したようなもの」になります。 「file や directory の削除」は Whiteouts と呼ばれる特別な仕様で表現されますが、特別に注意を払う必要があるのはそれくらいのようです。

Applying Changesets

  • Layer Changesets of media type application/vnd.oci.image.layer.v1.tar are applied, rather than simply extracted as tar archives.
  • Applying a layer changeset requires special consideration for the whiteout files.
  • In the absence of any whiteout files in a layer changeset, the archive is extracted like a regular tar archive.

cf. https://github.com/opencontainers/image-spec/blob/v1.0.1/layer.md#applying-changesets

ということで、OCI image の中身に目を通してみました。「Conatainer を走らせるために必要な情報(= 実行時のメタデータ + Layer 化された filesystem の情報)」が格納されてることがわかったかと思います。

Container Registry との通信内容を見てみる

ここまでで、「Container Image の内容」については把握できました。次に、「Container Registry から Container Image をどのように pull しているのか」を調べてみましょう。

現在、各種 Container Registry は Docker 社が公開している Docker Registry HTTP API V2 と呼ばれる仕様に従う形で Container Image の Pull を出来るようにしています。実は、「Container Image の Pull」にあたる操作はただの HTTP request であるため、$ curl を利用して実行する事ができます。ここでは、実際に $ curl で request してみることで、Container Registry との通信内容を見てみる事にしましょう。

なお、自分が試した範囲では、どの Container Image も OCI Image Format ではなく Docker Image Manifest V 2, Schema 2 に従う形の response を返してきました。ただ、OCI Image Format と Docker Image V2.2 は一部の mediaType 名を除いてほぼ同一なので、先ほど眺めた内容は理解に役立つはずです。

さて、実際に curl で request を送ってみましょう。対象 Container Image は何でも良いのですが、ここでは https://github.com/GoogleContainerTools/base-images-docker に記載されてる Debian の Container Image である gcr.io/google-appengine/debian9 を対象にしてみます。

まず、以下のように Container Registry の Authentication に必要な Token を取得します。この時、「google-appengine/debian9 の pull」という形で scope を指定しておきます。

$ export TOKEN=$(curl "https://gcr.io/v2/token?service=gcr.io&scope=repository:google-appengine/debian9:pull" | jq -r '.token')
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   453    0   453    0     0   1088      0 --:--:-- --:--:-- --:--:--  1088

次に、https://gcr.io/v2/<name>/manifests/<reference> へ先ほど取得した Token 付きで GET request を送ります。こうすると、Docker Image V2.2 における manifest file が取得できます。

$ curl -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/manifests/latest | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   528  100   528    0     0    469      0  0:00:01  0:00:01 --:--:--   469
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 463,
    "digest": "sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 47965538,
      "digest": "sha256:faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1"
    }
  ]
}

まず、config の中身を見てみましょう。

digest を利用して参照を辿る際は https://gcr.io/v2/<name>/blobs/<digest> へ request すれば良いです。実際に request してみると、以下のような response が返ってきます。先ほど OCI Image の中身を見てみた時と同様に、Container 実行に必要なメタデータが格納されていることが分かります。

$ curl -L -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/blobs/sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13    0    13    0     0     23      0 --:--:-- --:--:-- --:--:--    23
100   463  100   463    0     0    750      0 --:--:-- --:--:-- --:--:--   750
{
  "architecture": "amd64",
  "author": "Bazel",
  "created": "1970-01-01T00:00:00Z",
  "history": [
    {
      "author": "Bazel",
      "created": "1970-01-01T00:00:00Z",
      "created_by": "bazel build ..."
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:0a3dcb016bd8a852985044291de00ad6a6b94dcb0eac01b34b56afed409b9999"
    ]
  },
  "config": {
    "Cmd": [
      "/bin/sh",
      "-c",
      "/bin/bash"
    ],
    "Env": [
      "DEBIAN_FRONTEND=noninteractive",
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "PORT=8080"
    ]
  }
}

なお、注意点として、どうやら GCR は https://gcr.io/v2/<name>/blobs/<digest> への request では Google Cloud Storage への redirect response を返すようです。-L オプションを付けない場合は以下のような結果になることには留意してください。

$ curl --include -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/blobs/sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a
HTTP/2 302
docker-distribution-api-version: registry/2.0
location: https://storage.googleapis.com/artifacts.google-appengine.appspot.com/containers/images/sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a
content-type: application/json
date: Wed, 09 Dec 2020 13:38:51 GMT
server: Docker Registry
cache-control: private
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
accept-ranges: none
vary: Accept-Encoding

{"errors":[]}%

上記では config の取得を行いましたが、Layer (mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip のデータ)についても同様にhttps://gcr.io/v2/<name>/blobs/<digest> への request によって取得する事ができます。先ほどと同様に、tar コマンドで展開すると container 実行に利用される file を取得することが出来ます。

$ curl -L -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/blobs/sha256:faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1 --output /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13    0    13    0     0     28      0 --:--:-- --:--:-- --:--:--    28
100 45.7M  100 45.7M    0     0  13.6M      0  0:00:03  0:00:03 --:--:-- 20.1M

$ ls -la /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1
-rw-r--r--  1 minami  wheel  47965538 Dec  9 23:48 /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1

$ mkdir /tmp/rootfs
$ tar xvzf /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1 -C /tmp/rootfs/
$ ls /tmp/rootfs
bin   boot  dev   etc   home  lib   lib64 media mnt   opt   proc  root  run   sbin  srv   sys   tmp   usr   var

という事で、Container Registry との通信について、特に「Container Image の Pull」に絞って通信内容を見てみました。Docker Image V2.2 をベースにした通信である事、特に config や layer などがそれぞれの単位で通信できることなどが分かったかと思います。より詳しい内容が気になる場合は、Docker Registry HTTP API V2 を参照してみてください。

なお、「Container Image 全てをまとめた file を一括でダウンロードしないのは何故なのか?」という疑問についてですが、これは自分の理解では「Layer Cache を効かせた形での Image Pull を実現するため」だと捉えています。Layer のデータは巨大であるため最低限の通信で済ませたいというのが大前提にあり、そのために「コンテンツの中身を反映した digest 値を用いて、Layer ごとに通信する」という振る舞いになっているのだと思われます。

まとめ

OCI image を実際に作成して眺めて見ることで、Container Image について理解を深めました。また、curl で Container Registry との通信を行うことで、Container Registry との通信内容についても理解を深める事が出来ました。

ドキュメントを読むだけだとどうしても理解が曖昧になってしまいがちですが、実際に手を動かす事で具体的な動作をイメージ出来るようになります。このブログ自体は自分の理解のために試したことをまとめたものですが、誰か他の人にとっても理解を助けるものになっていれば幸いです。

なお、今回は Container Image + Container Registry 編でしたが、後日 Container Runtime についても「手を動かして調べた内容」についてまとめたいと思っています。特に、「Container Image から Container Runtime が利用する Filesystem Bundle への Conversion」や、「runc などの low-level Container Runtime の動作」、「containerd や CRI-O などの high-level Container Runtime の動作」について試したことをまとめる予定です。

補足: Vagrant での実験環境

以下のような Vagrantfile を使ってます。ubuntu-20.04 を使ってます。

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "bento/ubuntu-20.04"
end

参考文献

以下のブログは、ツールやコマンド、内容において大幅に参考にさせて頂きました。ありがとうございました。

コンテナ標準化の現状と Kubernetes との関係性について

コンテナ標準化が進んでいる事は知りつつも、標準化された仕様の具体的な内容についてはあまり知らない事に気づいたので、この機会に調べてみました。個人向けメモとして残しておきます。

余力があれば、後でもう少し詳細をまとめる予定です(docker image を OCI Image Format に変換して眺めてみたり、runc や containerd などを実際に動かしてみたり、containerd や CRI-O などの Container Runtime の実装に目を通してみたりしたので、その辺りについてもいつかまとめたいと思ってます)。

追記: 手を動かして調べた内容は以下の2つのブログにまとめました。

以下、調査した内容をまとめたメモです。

コンテナ標準と Open Container Initiative (OCI) について

コンテナ標準は Open Container Initiative (OCI) と呼ばれる団体によって仕様策定が進められている。 image format, (low-level) runtime については既に標準が存在している(v1.0.0 をリリース済み)。distribution (container registry 周り) については仕様策定中の状態(2020年12月6日時点で GitHub の tag では v1.0.0-rc1 が出ている状態)。

以下、 OCI が定める各種標準仕様について簡単にまとめる。

OCI Image Format Specification

  • Docker Image のような「container を記述する image format」の標準仕様。これは Docker Image の最新 format である Docker Image Manifest V2, Schema 2 をベースに標準化したもの。
  • 以下が Docker 社からの公式声明。
  • Container Registry とのデータのやり取りも OCI Image Format(およびそれの元になった Docker Image Manifest V2, Schema2)に準拠する形で行われる。
  • 後述する OCI Runtime Specification で利用される Filesystem Bundle を生成する Conversion 処理 についても仕様が策定されている。

OCI Runtime Specification

  • Container 管理を行う Container Runtime の標準仕様。後述するが、Low-Level Container Runtime と呼ばれるものはこの仕様に準拠している。
  • これは Docker 社が OCI に寄贈した "runC" とほぼ対応づくもの。
    • runc depends on and tracks the runtime-spec repository. We will try to make sure that runc and the OCI specification major versions stay in lockstep. This means that runc 1.0.0 should implement the 1.0 version of the specification.

    • https://github.com/opencontainers/runc
  • 「コンテナの configuration file およびコンテナの root filesystem をまとめたもの」である Filesystem Bundle や、OCI Runtime 準拠の Container Runtime で行える操作 について仕様を策定している。

OCI Distribution Specification

Container Runtime について

Container Runtime と呼ばれるものは複数存在するが、ものによって担当する layer が違う。以下、一例を紹介(注: 他にも Container Runtime と呼ばれるものはいくつかあるが、ここでは自分の理解のために調べた一部に限定している)。

runc

  • 前述した OCI Runtime Specification を素朴に実装したもの。後述する containerd と対比して、Low-Level Container Runtime と紹介をされる例を見かける。
  • Docker 社が「ツールとしての Docker」の一部を OSS として公開したのが出自。
  • OCI Runtime Specification で定められた Filesystem Bundle と呼ばれる「コンテナの configuration file およびコンテナの root filesystem をまとめたもの」を元に、コンテナの作成、削除、状態の取得などの操作が可能。
  • README の Using runc を参照して動かしてみると動作がイメージできる。

containerd

CRI-O

  • containerd とだいたい似たレイヤーを担当。「Kubernetes から利用されること(CRI に準拠していること)」を念頭に開発されてる。
  • runc を始めとして、いくつかのコンポーネントを組み合わせた実装になっている
    • The plan is to use OCI projects and best of breed libraries for different aspects:

      • Runtime: runc (or any OCI runtime-spec implementation) and oci runtime tools
      • Images: Image management using containers/image
      • Storage: Storage and management of image layers using containers/storage
      • Networking: Networking support through use of CNI
    • https://github.com/cri-o/cri-o#what-is-not-in-scope-for-this-project

Kubernetes が定める Container Runtime Interface (CRI) について

  • Kubernetes は Container Runtime Interface (CRI) と呼ばれる「独自で定義した API」を利用して、Container Runtime と通信を行う
  • CRI は基本的には「protocol buffer で記述された gRPC API」。container を操作するために必要な操作が RPC として定義されている。
  • アクティブに開発されてる Container Runtime は、CRI を実装している。一例は以下(注: Docker については Kubernetes 開発者によって dockershim と呼ばれる CRI サポート用のツールが実装されていた)。
  • 先週、 Docker が Kubernetes の Container Runtime としては Deprecated になった ことが話題になったが、これは dockershim のメンテナンスを将来のバージョンで止めるというアナウンス。
  • 自分が調べた限りでは、containerd, CRI-O などの Container Runtime はどれも複数の image format をサポートしていて、「docker build で生成した container image」は引き続き利用可能。
    • "Docker Image Manifest V2, Schema 2" と "OCI Image Format Specification" はどちらもサポートされている。
      • そもそもこれらは「ほぼ同一」ある(一部の mediaType が違うくらいで、1対1対応)
        • The OCI image manifest is also a registry image manifest that defines components that make up an image. The format is essentially the same as the Docker V2.2 format, with a few differences.

          • mediaType - must be set to application/vnd.oci.image.manifest.v1+json
          • config.mediaType - must be set to application/vnd.oci.image.config.v1+json
        • Each object in layers must have mediaType be either application/vnd.oci.image.layer.v1.tar+gzip or application/vnd.oci.image.layer.v1.tar.

        • https://containers.gitbook.io/build-containers-the-hard-way/#registry-format-oci-image-manifest
  • そのため、「docker build で作成した container image を container registry へ push して、k8s から利用する」という一連のワークフローに関していえば、containerd or CRI-O を使っても問題になる事は一切無い はず

2017、2018 年の振り返り

年が明けて 2019 年になりました。良い機会なので、2017、2018 年の2年間を振り返ってみたいと思います。

この2年間は色々な変化がありました。仕事面とプライベート面の2つに大きく分けてまとめてみます。

仕事面の振り返り

2017 年は、スカウトというプロダクトのグロースを主に行なっていました。弊社(Wantedly)ではプロダクトごとにチームがあり、自分はチームの中の Tech Lead & Product Manager として、「アイディアを機能に落とし込み、プロダクトを成長させる」という役割を担っていました。ポジションやチームは変わりつつも、2016年以前とはそれほど変わらない働き方でした。

一方、2018 年はインフラチームへ移籍をしたため、働き方が大きく変わりました。

弊社のインフラチームは、「SRE」と「技術基盤」という2つの役割を持っています。SRE としては、「安定したサービスの提供」に責任を持っており、SLI/SLA の策定や監視、障害対応、サービスの可用性やスケーラビリティを向上させる取り組みなどを行なっています。技術基盤としては、「エンジニアの生産性を最大化させること」に責任を持っており、様々な CLI ツールの作成や機械学習基盤の構築、CI/CD フローの構築・改善などを行なっています。また、弊社のマイクロサービスアーキテクチャをより良いものへ作り変えることや、社内横断的に必要なマイクロサービス(e.g. ユーザーアカウントを扱うサービス、ログインフローを扱うサービスなど)の開発も技術基盤として取り組んでいます。

インフラチームへ移ったことで、より社内横断的に必要な開発を行うようになりました。

移籍後にやった事は、細かいものを除くと以下の2つです。

  • 機械学習基盤の整備
  • 社内横断的に必要なマイクロサービスの開発

機械学習基盤の整備

機械学習基盤については、「社内の機械学習エンジニアの生産性を高める」事をゴールにプロジェクトを進めました。成果は Rejectcon や AWS Dev Day で発表しています。会社としても個人としても、やった事をちゃんと外に出せたのは良かったかなと思います。

https://speakerdeck.com/south37/number-rejectcon2018 https://speakerdeck.com/south37/number-awsdevday

また、「機械学習の評価データ管理」のためにいくつかツールを作っていて、それらは OSS として公開しました。

後で「プライベート面の振り返り」で詳しく書きますが、2017年頃から自分自身が OSS 活動に積極的になり、仕事でもそれが活かされてきたかなと思います。

社内横断的に必要なマイクロサービスの開発

社内横断的に必要なマイクロサービスの開発も行なっています。こちらはまだ道半ばで、プロジェクトとしてはちょうど半分ほど終わったところです。基本的には RailsAPI server を書いているのですが、社内で広く使われるサービスである事から、「API の schema を定義し、その schema を反映した server の実装および client library の提供」を行うようにしています。

API schema をどう記述するかについては JSON Schema や OpenAPI などいくつか候補を考えたのですが、最終的には protocol buffer の「.proto ファイル」を利用することにしました。理由としては、1つは後々 grpc へ移行したいと考えているためそれが自然に出来るようにしたかったから、もう1つは .proto ファイルから OpenAPI format の JSON を出力できるため、OpenAPI のエコシステムを利用できたからです(e.g. Swagge UI)。「後々 grpc へ移行」と書いたように、今は grpc は利用しておらず、.proto で記述した format の JSON response を返す API server と、JSON response を parse して object へ mapping する client library を提供しています。

すぐに grpc を利用しないのは、現時点では HTTP/2 用の proxy を社内的に導入していないため、RPC に HTTP/2 を利用する基盤が整っていないからです。ただ、基盤の導入は進めようとしており、それが出来たタイミングで grpc へ移行したいと考えています。

「.proto ファイルで JSON response の schema を記述する」という取り組みは世の中的にほとんど行われていないので、自分で仕組みを整えながら開発を進めています。細かい部分は以下のように gem として切り出して OSS にしています。

ただ、まだメインの仕組みについては公開できていません。メインの仕組みについても、後々整理して gem 化した上で、OSS にしたいと考えています。

仕事面の振り返りまとめ

インフラチームへ移った事で、働き方が大きく変わりました。 自分の興味の方向性ともあっており、良い変化だったと思います。

プライベート面の振り返り

プライベート面では、2017 年以降、OSS 活動に積極的になったと感じています。

きっかけは、2017 年 3 月に同僚や他社の知り合いと一緒に何気なく始めた Meguro.rb という地域 ruby コミュニティです。Meguro.rb は目黒近辺の会社で持ち回りでホストと会場提供をしながら LT と懇親会をしていて、最初は Wantedlyドリコム、Viibar の3社だけで始めました。開催するうちに Livesense, ラクスル, アカツキ, freee, Quipper, CaSy, SUPERSTUDIO とホスト企業がどんどん増えていき、今ではホスト企業は 10 社になりました。

Meguro.rb を始めてから、LT 発表する機会が増え、発表があると発表のために何か成果を出そうというモチベーションが生まれました。それが、OSS 活動にもつながったと思います。

2017、2018 年に行った OSS 活動について

2017、2018 年に行った OSS 活動について、いくつかまとめておこうと思います。

以下はいくつかのプロジェクトに対して送った PR の一部です。

個人的には、一番上の「Ruby 本体に contribute 出来たこと」が一番嬉しかったです(PR 自体は Close されているが、それは Ruby の contribution の扱いの方針によるもので、変更自体は別の commit で取り込まれている)。やったのは、「Ruby の文字列補完(String Interpolation)のパフォーマンス改善」です。Ruby は自分が最も書き慣れており仕事でも使っている言語なので、そのベースの機能を改善出来たことはとても良い体験でした。また、String Interpolration はベーシックな機能なので、インパクトも大きいと感じてます。

この contribution は、Meguro.rb でも紹介しました。

その他、自作のツール/ライブラリも OSS としていくつか公開しました。

rucc と yard2steep は Meguro.rb で発表したのですが、反響があって嬉しかったです。

rucc は Ruby で書いた C 言語コンパイラです。作ったきっかけは、コンパイラの勉強の為に 8cc という rui314 さん作の C コンパイラのコードを読んでいた事です。読むだけでなく写経した方が勉強になると考え、さらにただ C から C へ写経しても面白くないので Ruby へ移植する事にしました。class 構造の整理や ruby の機能による書き換え、足りない機能の追加などはありますが、ロジックの大部分は 8cc を参考にしています。時間はかかりましたが、とても楽しかったです。

yard2steep は、Ruby コード中の YARD アノーテーションを元に静的型チェックを行うためのツールです。2018 年の RubyKaigi で紹介されたsteep という「Ruby で静的型チェックを行うためのツール」向けに、型定義ファイルを Ruby コード中の YARDアノーテーションを解析して生成します。steep の発表の際に、「YARD を利用したい」という声を聞いて試しに作ってみたものです。実は、RubyKaigi 中のコード懇親会というイベントでプロトタイプを作ったのが project を始めたきっかけで、手応えがあったのでそのまま作りきりました。最初は parser を手で書いていて、それが楽しかったです(正確な文法を反映していなかったので、後で Ripper ベースに書き換えてしまいましたが)。

OSS 活動についてまとめ

2017 年から、OSS 活動に以前に比べて積極的になり、OSS に対する心理的ハードルがどんどん下がっています。良い変化だったと思います。

英語学習について

2018年は英語に対するモチベーションが上がって、DMM 英会話をやってみたり、1つの本を Audible と Kindle 両方で買って英語音声を聞きながら英語文章を読んだり、映画を英語音声/英語字幕で観たりし始めました。

きっかけは色々ありましたが、仕事の関係で AWS re:Invent に行けたのも大きいです。自分にとって初の海外カンファレンスで、行列で隣り合った現地のアメリカ人と話したり、Pab Crawl でアメリカに長く住む中国人と話したり出来ました。実は行く数ヶ月前から細々と英語学習をしていたのですが、現地で会話してみて「丁寧にゆっくり話してもらわないと聞き取りが難しい」ことを痛感し、より英語学習へのモチベーションが上がりました。

これも、良い変化だったと思います。

まとめ

2017、2018 年の2年間を振り返ると、仕事面/プライベート面の両方でポジティブな変化がたくさんありました。 良い取り組みについては今後も継続して取り組んでいきたいですし、今後もさらにポジティブな変化を起こしていきたいです。

ISUCON7本戦に出場してきました

ISUCON7本戦に出場してきました。 結果はスコア0、最終的にはFAILで終わるという惨敗でした。

出場メンバーは会社の同僚との3人組で、予選と同様に他の2人がアプリケーション担当、僕がインフラ担当という役割分担をしました。

残念な結果となってしまい、とても悲しい気持ちなのですが、その思いを昇華させるためにも当日やったこと、考えていたことなどを記録しておこうと思います。 尚、悲しい結果に終わったこともあり、後悔や言い訳の多い見苦しい文章となっています。あらかじめご了承ください。

当日の流れ

10:00 にレギュレーションが閲覧できる様になりました。 しばらくは運営側でid, passwordの配布に時間がかかっており、コンテストが始まらなかったので、その間に3人でレギュレーションの読み合わせを行いました。単純に読んでいるだけでも、複雑なアプリケーションで単純な最適化は難しそうだなーという印象を受けました。

最初の30分は僕が各サーバーのセットアップ(git管理下に置く、各人がsshできる様にauthorized_keysを配置する、netdataの監視を仕込むなど)を行い、他2人がDBのデータの確認とアプリケーションのコードを読んでの概要把握を行いました。

その後、最適化の方法をざっくり話し合い、以下の方針を決めました。

  • 方針1. calc_status が明らかにヘビーだから、なんらかの形でキャッシュする必要はありそう
  • 方針2. dbを触ってる部分はRedisに載せていくと良さそう
  • 方針3. roomごとにスコア計算は完全に独立しているから、roomごとにhostを振り分けて、hostごとにデータを持つ様にしたらthroughputが出そう(この時点では、redisをhostごとに建ててそこにデータをstoreすることを考えていたので、redisの負荷が減りそうくらいのイメージで捉えていた)
  • 方針4. static file配信はいつもの様にnginxでgzipで配信すると良さそう

今振り返るとこの時点での方針は悪くなかったと思うのですが、結果的には「方針4」以外は実装が思う様に進まず、ほぼmergeされる事はありませんでした。

自分が後悔している事の一つは、この時アプリケーションを読んでいた同僚から「ロックが必要になる」という事を言われた際に、ロックの方法についてもうちょっと自分もアプリケーションを読み込んだ上で議論すべきだったという事です。 自分はアプリケーションのコードは1行も読んでなくて、ただ「DBでロックを獲得するコードがある」と言われて議論した結果、DBを捨ててRedisを使うならRedisのincrでロック獲得を記述できそうという結論になりました。この「Redisのincrで実装したロック」は動作はしたのですが、毎回ロック獲得トライをRedisに問い合わせて行うというコストの高い動作の為か、スコアアップには繋がりませんでした。後で同僚からは「ロックはroom_nameごとに独立してかければ良かった」と聞かされて、そこまでの知識があればこの時点でroomごとに処理するprocessを分けて、ruby内部でmutexを活用しようという提案ができたかも。。。と後悔しています。

現実には、同僚の一人が「方針1」を、もう一人が「方針2」を進め、僕は「方針4」を行った後、便利スクリプト集(deployやログ閲覧、アクセスログ回収、各ホストの状態チェックなどをローカルのマシンからコマンド一発で実行できるもの)の整備やちょっとしたパラメータチューニング(pumaが10processで動いてmemoryを結構使っていたのでprocess数を減らしてthread数を増やしたりしてた)、2人とのペアプロなどを行なっていました。

「方針4」は一応スコアに効いて、9,600ぐらいになりました。しかし、それ以降僕らのチームのスコアが上がる事はありませんでした。

同僚が2人とも苦戦していたので、僕は定期的にペアプロでハマり解消を手伝っていました。

「方針1」を進めていた同僚が「途中まで出来た」といってベンチにかけてみたところ、スコアは上がらないどころか若干下がっている様に見えたので、その時はもう少し実装を進めてスコアを上げてからmergeしようという話になりました。結局、その後そのブランチがベンチを通る事はなく、彼は最後までそのブランチで作業し続けることになりました。(余談ですが、スコアが上がらなかったのは、おそらく前述の通りRedisをロックに利用していた為では無いかと疑っています)

「方針2」を進めていた同僚もRedisでロックをかけていて、こちらもベンチがなかなか通らずずっとハマっていました。最終的にロックを色々な箇所に設定する事でベンチは通ったみたいですが、それは終了の30分前であり、スコアも上がりませんでした。

(尚、「スコアが上がらなくてもmergeだけでもしておこう」といって「方針2」の一部はmergeされたのですが、最終スコアはFAILになってしまいました。不思議に思ってコードを見返すと、分かりやすく「引数が無いメソッドコール」というバグがありました。「ベンチ通ったからmergeした」と聞いたのに悲しい。。。)

結果としては、1日を通して有効な変更をほぼ入れられず、やりたいことが実現できないまま終了してしまったコンテストとなりました。

反省

自分の反省点はいろいろありますが、大きいのは以下の2つだと考えています。

1つ目は、自分がアプリケーションを全く読み込まなかったことです。「アプリケーションの仕様」を理解してないことから、途中で2人とペアプロする際も「コード的な間違いの指摘、修正」に終始してしまい、「アプリケーションの仕様の範囲で可能な限り効率的な実装を考える」という事が出来ませんでした。特に、今回の問題は効率的な実装をするにはスレッドプログラミングの知識が求められるものになっていたと思うので、自分の知識は本当はもっと活かせたんじゃ無いかと思っています。

2つ目は、自分がアプリケーションコードをほとんど書かなかった事です。これは1つ目の反省とも繋がるのですが、アプリケーションを読み込んでない状態では改善のアイディアを考えづらく、さらに残り時間が少なくなるほど「これからアプリケーションに着手する」といった手が取りづらくなり、結果としてほとんどアプリケーションを書きませんでした。アプリケーションがボトルネックというのは初めから分かっていたので、インフラ作業を捨てるというのも1つの選択肢だったように思います。

自分は普段はアプリケーションエンジニアとして仕事をしており、ISUCONは貴重な「インフラ知識をつけることができる、インフラオペレーションに慣れる事ができる」機会となっています。カーネルミドルウェアのチューニングをしてマシンを効率的に利用したり、デプロイツールなどを自作して様々なタスクを自動化するのは僕にとって楽しくて、ISUCONではインフラ担当としての参加にこだわっていました。ただ、今回はそのこだわりが良く無い方向に働いたな、と感じています。

結論

今年は悔しい結果となったので、また来年頑張りたいです!!!

謝辞

運営の皆さん、お疲れ様でした。綺麗に整理されたコードで複雑なアプリケーションが実装されており、運営の皆さんの気合を感じました。取り組みがいのある、良い問題だったと思います。ありがとうございました。