CUBE SUGAR CONTAINER

技術系のこと書きます。

kind (Kubernetes IN Docker) を使ってみる

今回は Kubernetes の開発で使われている公式ツールの kind を使ってみる。 このツールを使うと Docker のコンテナを使って Kubernetes のクラスタが素早く簡単に構築できる。

使った環境は次のとおり。

$ sw_vers                        
ProductName:    macOS
ProductVersion: 11.6
BuildVersion:   20G165
$ uname -m
arm64
$ kind version                             
kind v0.11.1 go1.16.4 darwin/arm64
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.5", GitCommit:"aea7bbadd2fc0cd689de94a54e5b7b758869d691", GitTreeState:"clean", BuildDate:"2021-09-15T21:10:45Z", GoVersion:"go1.16.8", Compiler:"gc", Platform:"darwin/arm64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.1", GitCommit:"5e58841cce77d4bc13713ad2b91fa0d961e69192", GitTreeState:"clean", BuildDate:"2021-05-21T23:06:30Z", GoVersion:"go1.16.4", Compiler:"gc", Platform:"linux/arm64"}
$ docker version
Client:
 Cloud integration: 1.0.17
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:55:20 2021
 OS/Arch:           darwin/arm64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.8
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.6
  Git commit:       75249d8
  Built:            Fri Jul 30 19:53:48 2021
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.4.9
  GitCommit:        e25210fe30a0a703442421b0f60afac609f950a3
 runc:
  Version:          1.0.1
  GitCommit:        v1.0.1-0-g4144b63
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

もくじ

下準備

はじめに Docker Desktop をインストールしておく。

$ brew install --cask docker

Docker を起動する。

$ open /Applications/Docker.app

docker version コマンドを実行する。 クライアントとサーバのバージョンが正しく表示されることを確認する。

$ docker version
Client:
 Cloud integration: 1.0.17
 Version:           20.10.8
 API version:       1.41
 Go version:        go1.16.6
 Git commit:        3967b7d
 Built:             Fri Jul 30 19:55:20 2021
 OS/Arch:           darwin/arm64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.8
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.6
  Git commit:       75249d8
  Built:            Fri Jul 30 19:53:48 2021
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.4.9
  GitCommit:        e25210fe30a0a703442421b0f60afac609f950a3
 runc:
  Version:          1.0.1
  GitCommit:        v1.0.1-0-g4144b63
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

続いて kind をインストールする。 macOS なら Homebrew で入る。

$ brew install kind

これで kind コマンドが使えるようになる。

$ kind version   
kind v0.11.1 go1.16.4 darwin/arm64

Kubernetes クラスタを作る

kind を使って Kubernetes のクラスタを作るには kind create cluster コマンドを使う。 最初に実行したときは Kubernetes のノード用のイメージファイルをダウンロードするので時間がかかる。

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.21.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) でクラスタができる。

$ kind get clusters
kind

kubectl コマンドの設定ファイルも自動で作られるので、すぐ使える状態になってる。

$  kubectl config get-contexts
CURRENT   NAME        CLUSTER     AUTHINFO    NAMESPACE
*         kind-kind   kind-kind   kind-kind   
$ kubectl config view --minify
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://127.0.0.1:49921
  name: kind-kind
contexts:
- context:
    cluster: kind-kind
    user: kind-kind
  name: kind-kind
current-context: kind-kind
kind: Config
preferences: {}
users:
- name: kind-kind
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED

デフォルトではシングルノード構成でクラスタができる。

$ kubectl get nodes
NAME                 STATUS   ROLES                  AGE    VERSION
kind-control-plane   Ready    control-plane,master   103s   v1.21.1

docker container list コマンドを使って稼働しているコンテナを見ると Kubernetes のコントロールプレーンのコンテナが見える。

$ docker container list
CONTAINER ID   IMAGE                  COMMAND                  CREATED         STATUS         PORTS                       NAMES
eec5aa7fa35d   kindest/node:v1.21.1   "/usr/local/bin/entr…"   2 minutes ago   Up 2 minutes   127.0.0.1:49921->6443/tcp   kind-control-plane

コンテナの中身を覗くと、必要なサービスが色々と立ち上がっているようだ。

$ docker container exec -it eec5aa7fa35d ss -tlnp
State     Recv-Q    Send-Q       Local Address:Port        Peer Address:Port    Process                                                                         
LISTEN    0         4096            172.18.0.2:2379             0.0.0.0:*        users:(("etcd",pid=675,fd=6))                                                  
LISTEN    0         4096             127.0.0.1:2379             0.0.0.0:*        users:(("etcd",pid=675,fd=5))                                                  
LISTEN    0         4096            172.18.0.2:2380             0.0.0.0:*        users:(("etcd",pid=675,fd=3))                                                  
LISTEN    0         4096             127.0.0.1:2381             0.0.0.0:*        users:(("etcd",pid=675,fd=10))                                                 
LISTEN    0         4096             127.0.0.1:10257            0.0.0.0:*        users:(("kube-controller",pid=607,fd=7))                                       
LISTEN    0         4096             127.0.0.1:10259            0.0.0.0:*        users:(("kube-scheduler",pid=546,fd=7))                                        
LISTEN    0         4096             127.0.0.1:42015            0.0.0.0:*        users:(("containerd",pid=183,fd=13))                                           
LISTEN    0         4096             127.0.0.1:10248            0.0.0.0:*        users:(("kubelet",pid=722,fd=23))                                              
LISTEN    0         4096             127.0.0.1:10249            0.0.0.0:*        users:(("kube-proxy",pid=957,fd=16))                                           
LISTEN    0         1024            127.0.0.11:33609            0.0.0.0:*                                                                                       
LISTEN    0         4096                     *:6443                   *:*        users:(("kube-apiserver",pid=593,fd=7))                                        
LISTEN    0         4096                     *:10256                  *:*        users:(("kube-proxy",pid=957,fd=18))                                           
LISTEN    0         4096                     *:10250                  *:*        users:(("kubelet",pid=722,fd=27))

ちなみに、推奨される環境としては Docker のランタイムに 6 ~ 8GB 以上のメモリを割り当てることが望ましいらしい。

以下のコマンドで、今のランタイムがどれくらい使えるか確認できる。 デフォルトでは 2GB になっている可能性があるので必要に応じて割り当てを増やそう。

$ docker stats --no-stream
CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT   MEM %     NET I/O           BLOCK I/O         PIDS
eec5aa7fa35d   kind-control-plane   132.22%   358.5MiB / 5.8GiB   6.04%     2.98kB / 1.34kB   18.1MB / 65.5kB   125

複数のクラスタを作る・壊す

特定の名前でクラスタを作りたいときは --name オプションを指定する。

$ kind create cluster --name kind-2

これで、2 つのクラスタができた。

$ kind get clusters
kind
kind-2

kubectl にも複数のクラスタが登録されている。

$ kubectl config get-contexts
CURRENT   NAME          CLUSTER       AUTHINFO      NAMESPACE
          kind-kind     kind-kind     kind-kind     
*         kind-kind-2   kind-kind-2   kind-kind-2   

クラスタを消すときは kind delete cluster コマンドを使う。

$ kind delete cluster --name kind-2

消したら、コンテキストを元のクラスタに切り替えておく。

$ kubectl config use-context kind-kind
Switched to context "kind-kind".
$ kubectl config current-context      
kind-kind

自前のコンテナイメージを使って Pod を立ち上げてみる

次は、試しに自前のコンテナイメージを使って Pod を立ち上げてみよう。

とりあえず、適当に WSGI でホスト名を返すアプリケーションを用意する。

$ cat << 'EOF' > server.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
from wsgiref.simple_server import make_server


def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    uname = os.uname()
    nodename = uname.nodename
    msg = f'Hello, World! by {nodename=}\n'
    return [msg.encode('ascii')]


def main():
    with make_server('', 8080, application) as httpd:
        print("Serving on port 8080...")
        httpd.serve_forever()

if __name__ == '__main__':
    main()
EOF

上記のアプリケーションを組み込んだ Dockerfile を用意する。

$ cat << 'EOF' > Dockerfile
FROM python:3.9

EXPOSE 8080

COPY server.py .

CMD python3 server.py
EOF

Docker イメージをビルドする。

$ docker build -t example/helloworld:0.1 .

手元のイメージは kind load docker-image コマンドを使ってクラスタに登録できる。

$ kind load docker-image example/helloworld:0.1

次のように自前のイメージを使って Pod を立ち上げるマニフェストファイルを用意する。 ポイントは imagePullPolicy: Never で、これで必ずローカルのイメージが使われるようになる。

$ cat << 'EOF' > helloworld.yaml
apiVersion: v1
kind: Pod
metadata:
  name: helloworld-pod
spec:
  containers:
  - name: helloworld-server
    image: example/helloworld:0.1
    imagePullPolicy: Never
EOF

マニフェストファイルを適用する。

$ kubectl apply -f helloworld.yaml
pod/helloworld-pod created

すると、次のように Pod が立ち上がる。

$ kubectl get pods
NAME             READY   STATUS    RESTARTS   AGE
helloworld-pod   1/1     Running   0          12s

ポートフォワーディングで Pod の TCP:8080 ポートを引き出してみよう。

$ kubectl port-forward helloworld-pod 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

別のターミナルからリクエストを送ると、ちゃんと HTTP のレスポンスが得られる。

$ curl http://localhost:8080
Hello, World! by nodename='helloworld-pod'

kubectl exec コマンドで Pod に入っても、ちゃんとプロセスが稼働していることが確認できる。

$ kubectl exec -it helloworld-pod -- /bin/bash
root@helloworld-pod:/# uname -a
Linux helloworld-pod 5.10.47-linuxkit #1 SMP PREEMPT Sat Jul 3 21:50:16 UTC 2021 aarch64 GNU/Linux
root@helloworld-pod:/# apt-get update && apt-get -y install iproute2
root@helloworld-pod:/# ss -tlnp
State                   Recv-Q                  Send-Q                                   Local Address:Port                                   Peer Address:Port                 Process                 
LISTEN                  0                       5                                              0.0.0.0:8080                                        0.0.0.0:*                     users:(("python3",pid=8,fd=3))
root@helloworld-pod:/# exit

マルチノードクラスタを作ってみる

kind の良いところは手軽にマルチノードクラスタも作れるところ。

一旦、シングルノードのクラスタは削除しておく。

$ kind delete cluster

そして、以下のような kind 用の設定ファイルを用意する。 これでコントロールプレーンとワーカーノード x2 のクラスタが作れる。

$ cat << 'EOF' > kind-multi-node-cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF

設定ファイルを使ってクラスタを作る。

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

kubectl cluster-info --context kind-kind-multi

Have a nice day! 👋

すると、次のように 3 つのノードから成るクラスタができる。

$ kubectl get nodes                            
NAME                       STATUS   ROLES                  AGE   VERSION
kind-multi-control-plane   Ready    control-plane,master   53s   v1.21.1
kind-multi-worker          Ready    <none>                 24s   v1.21.1
kind-multi-worker2         Ready    <none>                 24s   v1.21.1

確認すると、Docker コンテナも 3 つ稼働している。

$ docker container list
CONTAINER ID   IMAGE                  COMMAND                  CREATED              STATUS              PORTS                       NAMES
bcb2f337b802   kindest/node:v1.21.1   "/usr/local/bin/entr…"   About a minute ago   Up About a minute                               kind-multi-worker
eb32f4375353   kindest/node:v1.21.1   "/usr/local/bin/entr…"   About a minute ago   Up About a minute                               kind-multi-worker2
22da177bed79   kindest/node:v1.21.1   "/usr/local/bin/entr…"   About a minute ago   Up About a minute   127.0.0.1:50566->6443/tcp   kind-multi-control-plane

自前のイメージで Deployment を立ち上げてみる

マルチノードのクラスタを使って遊んでいこう。 試しに、複数の Pod を持った Deployment を作ってみる。

まずは先ほどビルドした Docker イメージをマルチノードのクラスタに登録する。

$ kind load docker-image example/helloworld:0.1 \
    --name kind-multi

そして、次のように Deployment 用のマニフェストファイルを用意する。 これで、Deployment x1 / ReplicaSet x1 / Pod x2 のリソースができる。

$ cat << 'EOF' > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-deployment
  labels:
    app: helloworld
spec:
  replicas: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - name: helloworld-server
        image: example/helloworld:0.1
        imagePullPolicy: Never
        ports:
        - containerPort: 8080
EOF

マニフェストファイルを適用する。

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

すると、次のようにそれぞれのオブジェクトができた。 Pod を見ると、ちゃんとそれぞれのワーカーで動作している様子が確認できる。

$ kubectl get deployments         
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-deployment   2/2     2            2           12s
$ kubectl get replicaset 
NAME                              DESIRED   CURRENT   READY   AGE
helloworld-deployment-d857c4cb6   2         2         2       25s
$ kubectl get pods      
NAME                                    READY   STATUS    RESTARTS   AGE
helloworld-deployment-d857c4cb6-8tvzk   1/1     Running   0          44s
helloworld-deployment-d857c4cb6-zcfl7   1/1     Running   0          44s

Pod に HTTP でアクセスする

続いては上記の Pod に HTTP でアクセスしてみる。 kubectl expose deployment コマンドで Service オブジェクトを作る。

$ kubectl expose deployment helloworld-deployment
service/helloworld-deployment exposed

これでアクセスするための IP アドレス (CLUSTER-IP) が割り当てられた。

$ kubectl get services -l app=helloworld            
NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
helloworld-deployment   ClusterIP   10.96.190.89   <none>        8080/TCP   102s

正し、この IP アドレスはクラスタ内部向けなので、Kind を実行しているホストからだと直接はアクセスできない。

$ ping -c 3 10.96.190.89  
PING 10.96.190.89 (10.96.190.89): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1

--- 10.96.190.89 ping statistics ---
3 packets transmitted, 0 packets received, 100.0% packet loss

そこで、代わりにクラスタのコンテナ経由でアクセスしてみよう。

$ docker container exec -it kind-multi-control-plane curl http://10.96.190.89:8080
Hello, World! by nodename='helloworld-deployment-d857c4cb6-zcfl7'
$ docker container exec -it kind-multi-control-plane curl http://10.96.190.89:8080
Hello, World! by nodename='helloworld-deployment-d857c4cb6-8tvzk'

ちゃんと、それぞれの Pod がリクエストを捌いていることが確認できた。

ちなみに、デフォルトでは --type=LoadBalancer な Service は作れないようだ。

$ kubectl delete service helloworld-deployment
$ kubectl expose deployment helloworld-deployment --type=LoadBalancer
service/helloworld-deployment exposed
$ kubectl get services -l app=helloworld                             
NAME                    TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
helloworld-deployment   LoadBalancer   10.96.93.210   <pending>     8080:30640/TCP   20s

上記のように、状態が <pending> となっており、IP アドレスが割り振られない。

確認が済んだら Service を削除しておこう。

$ kubectl delete service helloworld-deployment

オートヒーリングを確認する

続いてはオートヒーリングの様子を観察するために Pod を壊してみよう。

まずは今の Pod の状態をかくにんしておく。

$ kubectl get pods -l app=helloworld          
NAME                                    READY   STATUS    RESTARTS   AGE
helloworld-deployment-d857c4cb6-8tvzk   1/1     Running   0          8m9s
helloworld-deployment-d857c4cb6-zcfl7   1/1     Running   0          8m9s

ここで、Pod を片方削除してみよう。

$ kubectl delete pods helloworld-deployment-d857c4cb6-8tvzk
pod "helloworld-deployment-d857c4cb6-8tvzk" deleted

別のターミナルから Pod の状態を確認すると、指定した Pod が削除されて新たに別の Pod が作られる様子が見える。

$ kubectl get pods -l app=helloworld
NAME                                    READY   STATUS        RESTARTS   AGE
helloworld-deployment-d857c4cb6-8tvzk   0/1     Terminating   0          9m29s
helloworld-deployment-d857c4cb6-ndprj   1/1     Running       0          34s
helloworld-deployment-d857c4cb6-zcfl7   1/1     Running       0          9m29s

最終的には以下のように Pod が 2 つの状態で安定する。

$ kubectl get pods -l app=helloworld
NAME                                    READY   STATUS    RESTARTS   AGE
helloworld-deployment-d857c4cb6-ndprj   1/1     Running   0          45s
helloworld-deployment-d857c4cb6-zcfl7   1/1     Running   0          9m40s

また、ログを確認すると Pod が作り直されていることがわかる。

$ kubectl describe replicaset helloworld-deployment | tail
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age    From                   Message
  ----    ------            ----   ----                   -------
  Normal  SuccessfulCreate  11m    replicaset-controller  Created pod: helloworld-deployment-d857c4cb6-8tvzk
  Normal  SuccessfulCreate  11m    replicaset-controller  Created pod: helloworld-deployment-d857c4cb6-zcfl7
  Normal  SuccessfulCreate  2m49s  replicaset-controller  Created pod: helloworld-deployment-d857c4cb6-ndprj

そんな感じで。