CUBE SUGAR CONTAINER

技術系のこと書きます。

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

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

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

$ sw_vers     
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G4032
$ $ docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b
 Built:             Wed Mar 11 01:21:11 2020
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b
  Built:            Wed Mar 11 01:29:16 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
$ kind version
kind v0.8.1 go1.14.2 darwin/amd64
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:26:26Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.2", GitCommit:"52c56ce7a8272c798dbc29846288d7cd9fbae032", GitTreeState:"clean", BuildDate:"2020-04-30T20:19:45Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}

もくじ

下準備

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

$ brew cask install docker

docker version コマンドが正常に使えることを確認する。

$ docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b
 Built:             Wed Mar 11 01:21:11 2020
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b
  Built:            Wed Mar 11 01:29:16 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

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

$ brew install kind

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

$ kind version
kind v0.8.1 go1.14.2 darwin/amd64

Kubernetes クラスタを作る

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

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.18.2) 🖼 
 ✓ 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

Thanks for using kind! 😊

コマンドの実行が完了すると、デフォルトの名前 (kind) でクラスタができる。

$ kind get clusters
kind

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

$ kubectl config get-contexts
CURRENT   NAME          CLUSTER       AUTHINFO      NAMESPACE
*         kind-kind     kind-kind     kind-kind     
$ kubectl config get-contexts
CURRENT   NAME        CLUSTER     AUTHINFO    NAMESPACE
*         kind-kind   kind-kind   kind-kind
$ kubectl config view                 
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://127.0.0.1:58588
  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    master   69s   v1.18.2

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

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

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

$ docker exec -it bddde119ccb7 ss -tlnp
State      Recv-Q     Send-Q         Local Address:Port            Peer Address:Port                                                   
LISTEN     0          128               127.0.0.11:33945                0.0.0.0:*                                                      
LISTEN     0          128                127.0.0.1:37275                0.0.0.0:*         users:(("containerd",pid=130,fd=6))          
LISTEN     0          128                127.0.0.1:10248                0.0.0.0:*         users:(("kubelet",pid=743,fd=22))            
LISTEN     0          128                127.0.0.1:10249                0.0.0.0:*         users:(("kube-proxy",pid=992,fd=17))         
LISTEN     0          128               172.18.0.2:2379                 0.0.0.0:*         users:(("etcd",pid=691,fd=6))                
LISTEN     0          128                127.0.0.1:2379                 0.0.0.0:*         users:(("etcd",pid=691,fd=5))                
LISTEN     0          128               172.18.0.2:2380                 0.0.0.0:*         users:(("etcd",pid=691,fd=3))                
LISTEN     0          128                127.0.0.1:2381                 0.0.0.0:*         users:(("etcd",pid=691,fd=10))               
LISTEN     0          128                127.0.0.1:10257                0.0.0.0:*         users:(("kube-controller",pid=553,fd=6))     
LISTEN     0          128                127.0.0.1:10259                0.0.0.0:*         users:(("kube-scheduler",pid=598,fd=6))      
LISTEN     0          128                        *:10250                      *:*         users:(("kubelet",pid=743,fd=21))            
LISTEN     0          128                        *:10251                      *:*         users:(("kube-scheduler",pid=598,fd=5))      
LISTEN     0          128                        *:6443                       *:*         users:(("kube-apiserver",pid=644,fd=5))      
LISTEN     0          128                        *:10252                      *:*         users:(("kube-controller",pid=553,fd=5))     
LISTEN     0          128                        *:10256                      *:*         users:(("kube-proxy",pid=992,fd=15))      

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

以下のコマンドで、今のランタイムがどれくらい使えるか確認できる。

$ docker stats --no-stream

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

特定の名前でクラスタを作りたいときは --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.8

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' > example-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello-pod
spec:
  containers:
  - name: hello-server
    image: example/helloworld:0.1
    imagePullPolicy: Never
EOF

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

$ kubectl apply -f example-pod.yaml

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

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

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

$ kubectl port-forward hello-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='hello-pod'

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

$ kubectl exec -it hello-pod /bin/bash
root@hello-pod:/# uname -a
Linux hello-pod 4.19.76-linuxkit #1 SMP Tue May 26 11:42:35 UTC 2020 x86_64 GNU/Linux
root@hello-pod:/# ss -tlnp
State     Recv-Q    Send-Q       Local Address:Port        Peer Address:Port    
LISTEN    0         5                  0.0.0.0:8080             0.0.0.0:*        users:(("python3",pid=8,fd=3))

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

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

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

$ kind delete cluster

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

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

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

$ kind create cluster --name kind-multi --config multi-node-cluster.yaml

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

$ kubectl get nodes
NAME                       STATUS   ROLES    AGE   VERSION
kind-multi-control-plane   Ready    master   89s   v1.18.2
kind-multi-worker          Ready    <none>   54s   v1.18.2
kind-multi-worker2         Ready    <none>   56s   v1.18.2

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

$ docker container list
CONTAINER ID        IMAGE                  COMMAND                  CREATED              STATUS              PORTS                       NAMES
2fb405206d26        kindest/node:v1.18.2   "/usr/local/bin/entr…"   About a minute ago   Up About a minute                               kind-multi-worker
b657e6562c59        kindest/node:v1.18.2   "/usr/local/bin/entr…"   About a minute ago   Up About a minute                               kind-multi-worker2
9792c6353e76        kindest/node:v1.18.2   "/usr/local/bin/entr…"   About a minute ago   Up About a minute   127.0.0.1:61270->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' > example-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-pod
  template:
    metadata:
      labels:
        app: hello-pod
    spec:
      containers:
      - name: hello-server
        image: example/helloworld:0.1
        imagePullPolicy: Never
        ports:
        - containerPort: 8080
EOF

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

$ kubectl apply -f example-deploy.yaml

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

$ kubectl get deployments                       
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
example-deploy   2/2     2            2           14s
$ kubectl get replicaset
NAME                        DESIRED   CURRENT   READY   AGE
example-deploy-7d746dc7f9   2         2         2       61s
$ kubectl get pods -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP           NODE                 NOMINATED NODE   READINESS GATES
example-deploy-7d746dc7f9-2qg9t   1/1     Running   0          91s   10.244.2.2   kind-multi-worker2   <none>           <none>
example-deploy-7d746dc7f9-7csnf   1/1     Running   0          91s   10.244.1.2   kind-multi-worker    <none>           <none>

Pod に HTTP でアクセスする

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

$ kubectl expose deployment example-deploy
service/example-deploy exposed

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

$ kubectl get services
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
example-deploy   ClusterIP   10.98.227.119   <none>        8080/TCP   6s
kubernetes       ClusterIP   10.96.0.1       <none>        443/TCP    21m

この IP アドレスはクラスタ内部向けなので、コンテナ経由でアクセスしてみよう。

$ docker exec -it kind-multi-control-plane curl http://10.98.227.119:8080
Hello, World! by nodename='example-deploy-7d746dc7f9-dkcwh'
$ docker exec -it kind-multi-control-plane curl http://10.98.227.119:8080
Hello, World! by nodename='example-deploy-7d746dc7f9-7csnf'

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

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

$ kubectl delete service example-deploy
$ kubectl expose deployment example-deploy \
    --type=LoadBalancer
$ kubectl get services
NAME             TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
example-deploy   LoadBalancer   10.111.111.250   <pending>     8080:32633/TCP   9s
kubernetes       ClusterIP      10.96.0.1        <none>        443/TCP          42m

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

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

今ある Pod を片方削除してみる。

$ kubectl delete pod example-deploy-7d746dc7f9-2qg9t
pod "example-deploy-7d746dc7f9-2qg9t" deleted

ちょっと待つと消した分の Pod が別の識別子で増えている。

$ kubectl get pods -o wide                          
NAME                              READY   STATUS    RESTARTS   AGE     IP           NODE                NOMINATED NODE   READINESS GATES
example-deploy-7d746dc7f9-7csnf   1/1     Running   0          4m33s   10.244.1.2   kind-multi-worker   <none>           <none>
example-deploy-7d746dc7f9-dkcwh   1/1     Running   0          109s    10.244.1.3   kind-multi-worker   <none>           <none>

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

$ kubectl describe replicaset example-deploy | tail -n 7
Events:
  Type    Reason            Age    From                   Message
  ----    ------            ----   ----                   -------
  Normal  SuccessfulCreate  9m10s  replicaset-controller  Created pod: example-deploy-7d746dc7f9-2qg9t
  Normal  SuccessfulCreate  9m10s  replicaset-controller  Created pod: example-deploy-7d746dc7f9-7csnf
  Normal  SuccessfulCreate  6m26s  replicaset-controller  Created pod: example-deploy-7d746dc7f9-dkcwh

そんな感じで。