Kubernetes勉強 その3（DeploymentとService）


引き続き「Kubernetes完全ガイド」読んでお勉強。

Pod と ReplicaSet と Deployment

`Pod` はリソースの最小単位であり、`ReplicaSet` は `Pod` を管理するリソース。 `ReplicaSet` は `Pod` が停止すると自動的に再起動したりする。
`Deployment` は、 `Pod` に含まれるコンテナをバージョンアップする際に、古いコンテナを徐々に減らし、新しいコンテナを徐々に増やすなどの管理をする。

## Deployment

* Deploymentを使うと、ワーカーのバージョンアップを行う場合に旧バージョンを徐々に減らし、新バージョンを徐々に増やすローリングアップデートが行える。

## Deploymentの作成

以下のようなファイルを作る。

```yml:sample-deployment.yml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80
```

コンテナを作成する。

```
$ kubectl apply -f sample-deployment.yml 

deployment.apps "sample-deployment" created
```

状況を確認

```
$ kubectl get pods

NAME                                 READY     STATUS    RESTARTS   AGE
sample-deployment-86b68b4c5b-8v4dg   1/1       Running   0          41s
sample-deployment-86b68b4c5b-cnj45   1/1       Running   0          41s
sample-deployment-86b68b4c5b-pjpln   1/1       Running   0          41s
```

ポッドが３つできています。試しに一個落としてみます。

```
$ kubectl delete pod sample-deployment-86b68b4c5b-8v4dg

pod "sample-deployment-86b68b4c5b-8v4dg" deleted
```

状況を確認してみましょう。

```
$ kubectl get pods

NAME                                 READY     STATUS    RESTARTS   AGE
sample-deployment-86b68b4c5b-cnj45   1/1       Running   0          2m
sample-deployment-86b68b4c5b-pjpln   1/1       Running   0          2m
sample-deployment-86b68b4c5b-vvcpj   1/1       Running   0          5s
```

おお、新しく `sample-deployment-86b68b4c5b-vvcpj` が起動しましたね。これによって、 `Deployment` は `ReplicaSet` と同等の再起動機能を持っていることがわかりました。

では、nginxを1.12から1.13にアップデートしてみます。

```
$ kubectl set image deployment sample-deployment nginx-container=nginx:1.13

deployment.apps "sample-deployment" image updated
```

様子を見てみましょう。

```
$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-cnj45   1/1       Running       0          7m
sample-deployment-86b68b4c5b-pjpln   1/1       Running       0          7m
sample-deployment-86b68b4c5b-vvcpj   1/1       Terminating   0          5m
sample-deployment-d5b55f699-jhstp    0/1       Pending       0          1s
sample-deployment-d5b55f699-pxpfq    1/1       Running       0          5s

$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-cnj45   1/1       Running             0          7m
sample-deployment-86b68b4c5b-pjpln   1/1       Running             0          7m
sample-deployment-86b68b4c5b-vvcpj   0/1       Terminating         0          5m
sample-deployment-d5b55f699-jhstp    0/1       ContainerCreating   0          3s
sample-deployment-d5b55f699-pxpfq    1/1       Running             0          7s

$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-cnj45   1/1       Terminating         0          7m
sample-deployment-86b68b4c5b-pjpln   1/1       Running             0          7m
sample-deployment-86b68b4c5b-vvcpj   0/1       Terminating         0          5m
sample-deployment-d5b55f699-jhstp    1/1       Running             0          5s
sample-deployment-d5b55f699-k8fvd    0/1       ContainerCreating   0          2s
sample-deployment-d5b55f699-pxpfq    1/1       Running             0          9s

$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-cnj45   0/1       Terminating   0          7m
sample-deployment-86b68b4c5b-pjpln   1/1       Terminating   0          7m
sample-deployment-86b68b4c5b-vvcpj   0/1       Terminating   0          5m
sample-deployment-d5b55f699-jhstp    1/1       Running       0          7s
sample-deployment-d5b55f699-k8fvd    1/1       Running       0          4s
sample-deployment-d5b55f699-pxpfq    1/1       Running       0          11s

$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-cnj45   0/1       Terminating   0          7m
sample-deployment-86b68b4c5b-pjpln   0/1       Terminating   0          7m
sample-deployment-86b68b4c5b-vvcpj   0/1       Terminating   0          5m
sample-deployment-d5b55f699-jhstp    1/1       Running       0          9s
sample-deployment-d5b55f699-k8fvd    1/1       Running       0          6s
sample-deployment-d5b55f699-pxpfq    1/1       Running       0          13s


$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-pjpln   0/1       Terminating   0          7m
sample-deployment-d5b55f699-jhstp    1/1       Running       0          15s
sample-deployment-d5b55f699-k8fvd    1/1       Running       0          12s
sample-deployment-d5b55f699-pxpfq    1/1       Running       0          19s

$ kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
sample-deployment-d5b55f699-jhstp   1/1       Running   0          21s
sample-deployment-d5b55f699-k8fvd   1/1       Running   0          18s
sample-deployment-d5b55f699-pxpfq   1/1       Running   0          25s
```

おお、新しいコンテナが立ち上がると同時に、古いコンテナが徐々に消えていきます。想定していた通りの動きとはいえ、なかなか感動ものですね。


さて、いまはコマンドでコンテナのバージョンを指定しましたが、ファイルでバージョンを上げた場合の動きを見てみましょう。

ファイルを書き換えます。書き換えるのはnginxのバージョンのみ。

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.13
          ports:
            - containerPort: 80
```

続いて、変更を反映します。
まずは、一旦全てのコンテナを削除して、 `nginx:1.12` で立ち上げ直しましょう。

```
$ kubectl delete -f sample-deployment.yml 
deployment.apps "sample-deployment" deleted

$ kubectl apply -f sample-deployment.yml 
deployment.apps "sample-deployment" created

$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-drg4l   0/1       ContainerCreating   0          3s
sample-deployment-86b68b4c5b-dwb28   0/1       ContainerCreating   0          3s
sample-deployment-86b68b4c5b-k6vkt   0/1       ContainerCreating   0          3s
$ 
$ 
$ vi sample-deployment.yml 
$ kubectl get pods
NAME                                 READY     STATUS    RESTARTS   AGE
sample-deployment-86b68b4c5b-drg4l   1/1       Running   0          27s
sample-deployment-86b68b4c5b-dwb28   1/1       Running   0          27s
sample-deployment-86b68b4c5b-k6vkt   1/1       Running   0          27s
```

ここで、設定ファイルを修正して `nginx:1.13` にアップデートします。

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.13
          ports:
            - containerPort: 80
```


```
$ kubectl apply -f sample-deployment.yml 
deployment.apps "sample-deployment" configured

$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-drg4l   1/1       Running             0          1m
sample-deployment-86b68b4c5b-dwb28   1/1       Running             0          1m
sample-deployment-86b68b4c5b-k6vkt   1/1       Running             0          1m
sample-deployment-d5b55f699-rbftp    0/1       ContainerCreating   0          5s

$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-drg4l   1/1       Running       0          1m
sample-deployment-86b68b4c5b-dwb28   1/1       Terminating   0          1m
sample-deployment-86b68b4c5b-k6vkt   1/1       Running       0          1m
sample-deployment-d5b55f699-rbftp    1/1       Running       0          13s
sample-deployment-d5b55f699-xvm78    0/1       Pending       0          2s

$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-drg4l   0/1       Terminating         0          2m
sample-deployment-86b68b4c5b-k6vkt   1/1       Running             0          2m
sample-deployment-d5b55f699-rbftp    1/1       Running             0          49s
sample-deployment-d5b55f699-rwm49    0/1       ContainerCreating   0          13s
sample-deployment-d5b55f699-xvm78    1/1       Running             0          38s

$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-k6vkt   1/1       Terminating   0          2m
sample-deployment-d5b55f699-rbftp    1/1       Running       0          1m
sample-deployment-d5b55f699-rwm49    1/1       Running       0          30s
sample-deployment-d5b55f699-xvm78    1/1       Running       0          55s

$ kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
sample-deployment-d5b55f699-rbftp   1/1       Running   0          1m
sample-deployment-d5b55f699-rwm49   1/1       Running   0          1m
sample-deployment-d5b55f699-xvm78   1/1       Running   0          1m
$ 

```

## ロールバック

なんらかの理由で、古いバージョンに戻さなくてはならなくなったとします。まあよくある話です。

```
$ kubectl rollout undo deployment sample-deployment --to-revision 0

deployment.apps "sample-deployment" 
```

`--to-revision` に `0` を指定した場合、一つ前に戻ります。


状況を確認します。

```
$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-zlnnf   0/1       ContainerCreating   0          3s
sample-deployment-d5b55f699-rbftp    1/1       Running             0          12m
sample-deployment-d5b55f699-rwm49    1/1       Running             0          12m
sample-deployment-d5b55f699-xvm78    1/1       Running             0          12m

$ kubectl get pods
NAME                                 READY     STATUS              RESTARTS   AGE
sample-deployment-86b68b4c5b-zlnnf   0/1       ContainerCreating   0          7s
sample-deployment-d5b55f699-rbftp    1/1       Running             0          12m
sample-deployment-d5b55f699-rwm49    1/1       Running             0          12m
sample-deployment-d5b55f699-xvm78    1/1       Running             0          12m

$ kubectl get pods
NAME                                 READY     STATUS        RESTARTS   AGE
sample-deployment-86b68b4c5b-4skwv   1/1       Running       0          6s
sample-deployment-86b68b4c5b-cfmkh   1/1       Running       0          12s
sample-deployment-86b68b4c5b-zlnnf   1/1       Running       0          20s
sample-deployment-d5b55f699-rbftp    1/1       Terminating   0          12m

$ kubectl get pods
NAME                                 READY     STATUS    RESTARTS   AGE
sample-deployment-86b68b4c5b-4skwv   1/1       Running   0          13s
sample-deployment-86b68b4c5b-cfmkh   1/1       Running   0          19s
sample-deployment-86b68b4c5b-zlnnf   1/1       Running   0          27s
```

新しいコンテナが立ち上がりました。

## Serviceによる内部IPアドレス

先ほどのdeploymentに加えて、以下のServiceを定義して起動する。

```yaml:sample-clusterip.yml
apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app
```

ちなみに、おさらいしておくと、deploymentのほうはこんな定義になってるはず。

```yaml:sample-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.13
          ports:
            - containerPort: 80
```

ここで、deplymentのほうで `app: sample-app` としておいて、serviceの方で、 `selector: app: sample-app` としているので、serviceは自身の8080番ポートへ届いたパケットを、deploymentの3つのレプリカへ転送することになる。

まず、念のためdeploymentをapply

```
$ kubectl apply -f sample-deployment.yml
```

続いて、serviceをapply

```
$ kubectl apply -f sample-clusterip.yml
```

これでServiceとDeploymentか起動してるはず。

```
$ kubectl get pods,services

NAME                                    READY     STATUS    RESTARTS   AGE
pod/sample-deployment-d5b55f699-dpdmz   1/1       Running   0          3m
pod/sample-deployment-d5b55f699-sh452   1/1       Running   0          3m
pod/sample-deployment-d5b55f699-x557b   1/1       Running   0          3m

NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/kubernetes         ClusterIP   10.96.0.1        <none>        443/TCP    10h
service/sample-clusterip   ClusterIP   10.111.247.243   <none>        8080/TCP   8s
```

443番ポートにも何かサービスが立ち上がってるけど、これはデフォルトで立ち上がってるものだろうか...?
それ以外はpodもserviceも先ほど定義したものが立ち上がっています。

Serviceの状態を確認します。

```
$ kubectl describe svc sample-clusterip

Name:              sample-clusterip
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-clusterip","namespace":"default"},"spec":{"ports":[{"name":"http-port",...
Selector:          app=sample-app
Type:              ClusterIP
IP:                10.111.247.243
Port:              http-port  8080/TCP
TargetPort:        80/TCP
Endpoints:         172.17.0.4:80,172.17.0.5:80,172.17.0.6:80
Session Affinity:  None
Events:            <none>
```

`IP` がServiceのIPアドレス、 `Endpoints` がトラフィックを割り振る各コンテナのIPアドレス、 `Port` はServiceのポート番号、 `TargetPort` は各コンテナのサービスポートみたい。

各コンテナのhostnameをnginxのwwwrootにコピーして、アクセスしたコンテナがわかるようにします。

```
$ for PODNAME in `kubectl get pods -l app=sample-app -o jsonpath='{.items[*].metadata.name}'`; do
  kubectl exec -it ${PODNAME} -- cp /etc/hostname /usr/share/nginx/html/index.html;                                           done
```

ClusterIPは外部からはアクセスできないので、一時コンテナを立ち上げてチェック

```
$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
sample-deployment-d5b55f699-sh452

$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
sample-deployment-d5b55f699-x557b

$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
sample-deployment-d5b55f699-x557b

$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
sample-deployment-d5b55f699-sh452

$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
sample-deployment-d5b55f699-x557b

$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
sample-deployment-d5b55f699-dpdmz

$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://10.111.247.243:8080
```

アクセスするごとにコンテナ名が変わっているので、複数コンテナに割り振られているらしい。

### 名前解決

上記はIPアドレスでアクセスしたが、kubernetesでは内部DNSによって自動的に名前付けが行われるらしい。
ためしに今回定義した `sample-clusterip` という名前でアクセスしてみる。

```
$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://sample-clusterip:8080

sample-deployment-d5b55f699-x557b
```

おお、何も名前解決のための設定をしていないのに解決できた。アプリケーションはこちらを使って接続先を定義した方が、毎回書き換える必要がなくてよさそうだ。

ただし、先ほどは同じネームスペース内からのアクセスだから `sample-clusterip` で良かったが、本来はこちらが正確らしい。

```
$ kubectl run --image=centos:6 --restart=Never --rm -i testpod -- curl -s http://sample-clusterip.default.svc.cluster.local:8080

sample-deployment-d5b55f699-dpdmz
```

`[Serivice名].[Namespace名].svc.cluster.local` が正式なFQDNになる。

## ExternalIPによるサービス内部公開

ClusterIPでは内部ロードバランサを定義しましたが、これはあくまで内部で使うのものなので、外部からアクセスすることはできない。
けど、Webサーバーなどで外部公開が目的のものは外部に公開できなければならない。この場合はExternalIPを使う。
ExternalIPといっても実態はClusterIPで、ClusterIPに `spec.externalIPs` にノードのIPアドレスを定義すると使えるようになる。

最初にノードのIPアドレスを調べる。minikubeを使っている場合は、仮想環境のアドレスであってMacのアドレスではないので注意が必要。

```
$ kubectl cluster-info

Kubernetes master is running at https://192.168.99.100:8443
KubeDNS is running at https://192.168.99.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
```

これをみると、どうやらノードのIPアドレスは `192.168.99.100` のようだ。
このアドレスを新たに作った `sample-externalip.yml` に設定してやる。

```yaml:sample-externalip.yml
apiVersion: v1
kind: Service
metadata:
  name: sample-externalip
spec:
  type: ClusterIP
  externalIPs:
    -  192.168.99.100
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app
```

applyする。

```
$ kubectl apply -f sample-externalip.yml 
```

状態を見てみよう。

```
$ kubectl describe svc sample-externalip

Name:              sample-externalip
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-externalip","namespace":"default"},"spec":{"externalIPs":["192.168.99.1...
Selector:          app=sample-app
Type:              ClusterIP
IP:                10.103.102.209
External IPs:      192.168.99.100
Port:              http-port  8080/TCP
TargetPort:        80/TCP
Endpoints:         172.17.0.4:80,172.17.0.5:80,172.17.0.6:80
Session Affinity:  None
Events:            <none>
```

External IPs として設定したIPアドレスが割り付けられている。
curlで外部からアクセスしてみよう。


```
$ curl http://192.168.99.100:8080
sample-deployment-d5b55f699-dpdmz

$ curl http://192.168.99.100:8080
sample-deployment-d5b55f699-sh452

$ curl http://192.168.99.100:8080
sample-deployment-d5b55f699-dpdmz

$ curl http://192.168.99.100:8080
sample-deployment-d5b55f699-x557b

$ curl http://192.168.99.100:8080
sample-deployment-d5b55f699-dpdmz
```

リクエストが各コンテナに割り振られているのが確認できた。

## NodePortによる、全ノードでのListen

ExternalIPを使うと、指定したIPアドレスのノードでListenしますが、NodePortを使うと参加している全てのノードの指定ポートで待機するようになるみたいです。やってみましょう。

```yaml:sample-nodeport.yml
apiVersion: v1
kind: Service
metadata:
  name: sample-nodeport
spec:
  type: NodePort 
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30080
  selector:
    app: sample-app
```

ExternalIPの定義とよく似ているが、違うのは

* `spec.type` に `NodePort` を指定。
* `spec.externalIPs` を定義しない。
* `spec.ports.nodePort` に公開するポート番号を指定する。

のようです。

さっそくapplyします。

```
$ kubectl apply -f sample-nodeport.yml
 
service "sample-nodeport" created
```

curlでアクセスしてみる。

```
$ curl http://192.168.99.100:30080

sample-deployment-d5b55f699-dpdmz
```

ふう。
とりえあず動作が確認できたので削除。

```
$ kubectl delete -f sample-nodeport.yml
service "sample-nodeport" deleted

$ kubectl delete -f sample-cluster.yaml 
service "sample-clusterip" deleted
```

## LoadBalancerによる外部公開

サービスを外部公開する場合、結局これになるんだろうな〜、っていう。いわゆるロードバランサー。
LoadBalancerの実装はKubernetesのノードがどこで動いているかに依存するようです。
まだ試してませんが、AWSであればLoadBalancerを定義するとElasticIPを取って来てくれるんでしょうかね。

設定はこんな感じで

```yaml:sample-loadbalancer.yml
apiVersion: v1
kind: Service
metadata:
  name: sample-lb
spec:
  type: LoadBalancer
  loadBalancerIP: 192.168.99.100
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30082
  selector:
    app: sample-app
```

loadbalancerIPは定義しなくてもいいけど、普通は外部公開するサービスであればIPアドレスは固定になるので定義した。
定義しない場合は自動的に割り振られる。
起動します。

```
$ kubectl apply -f sample-loadbalancer.yml 

service "sample-lb" created
```

サービスの様子を確認します。

```
$ kubectl get services

NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          14h
sample-lb    LoadBalancer   10.106.15.138   <pending>     8080:30082/TCP   16m
```

あれ、sample-lbのEXTERNAL-IPがpendingのまま。

このままではどうすればいいかわからないので、minikubeコマンドでURL取得。

```
$ minikube service sample-lb --url

http://192.168.99.100:30082
```

とうことで、curlでアクセス。

```
$ curl http://192.168.99.100:30082
sample-deployment-d5b55f699-sh452

$ curl http://192.168.99.100:30082
sample-deployment-d5b55f699-x557b
```

ほーん。