KubernetesのRolling updateを安全に行う

## 背景

リリースの度にユーザのリクエストを取りこぼすのは、単純な設定漏れですので図を交えながらどういった設定が必要か解説していきます。

https://github.com/TakiTake/kubernetes-rolling-update-safely
デモアプリのコードや、Kubernetesの設定ファイルは上記のリポジトリに上げてあります。

## 実行環境

```
minikube version: v0.28.2
```

## 事前準備

```shell
# start minikube
$ minikube start

# Dockerのホストをminikube内のDockerに向ける
# こうしておくと、docker buildしたimageを直接使えるので
# Docker hub等にアップロードする必要が無くなる
$ eval $(minikube docker-env)

# build demo application
$ docker build -t takitake/demo demo
```

## デモシナリオ

### 初期設定

基本的には、シンプルなspring-boot製のアプリをデプロイしているだけです。
デモ用に下記の設定も足しています。

* imagePullPolicyにIfNotPresentを指定することで、ローカルにあるDocker imageを使用する
* どのPodからレスポンスが返ってきているかわかりやすくするために、Pod名を環境変数にセットしている

```
$ cat demo-manifest/1-0.default.deploy.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: demo
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
      annotations:
        appVersion: v1.0
    spec:
      containers:
      - image: takitake/demo
        imagePullPolicy: IfNotPresent # Don't pull container image everytime
        name: demo
        env:
        - name: POD_NAME # Passing Pod name as a environment varible
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
```

デプロイしてみます。ステータスが *ContainerCreating* から *Running*　に遷移して無事Podが動けば成功です。

```sh
$ kubectl apply -f demo-manifest/1-0.default.deploy.yml && kubectl get pods -w
deployment.extensions "demo" created
NAME                    READY     STATUS              RESTARTS   AGE
demo-67fb9964f4-xmrwj   0/1       ContainerCreating   0          0s
demo-67fb9964f4-xmrwj   1/1       Running   0         1s
```

立ち上がったアプリにアクセスするために、Kunernetes Serviceも作成しましょう。

```sh
$ kubectl apply -f demo-manifest/service.yml
service "demo" created

$ kubectl get service demo
NAME      TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
demo      NodePort   10.111.204.139   <none>        8080:32569/TCP   23s

$ minikube status
minikube: Running
cluster: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.100
```

今回は、minikubeを利用しているのでlocalhostではなく、minikubeのIPとNodePortを指定する必要があります。よって、URLは [http://192.168.99.100:32569/](http://192.168.99.100:32569/) となります。もしくは、`minikube service demo` と打てば、自動でページが開きます。

次に、Rolling updateで新しいバージョンのアプリをデプロイするために、バージョンだけ更新したYAMLファイルをデプロイします。

```sh
$ diff "demo-manifest/1-0.default.deploy.yml" "demo-manifest/1-1.default.deploy.yml"
17c17
<         appVersion: v1.0
---
>         appVersion: v1.1
```

別ターミナルで、リクエストを投げ続けることで問題なくアップデートが行えたかどうか確認可能です。

```sh
# １秒sleepを挟みながら、リクエストを投げ続けるスクリプト
# Response Body（今回はPod名）、日付、HTTP Codeを出力
$ export DEMO_URL=$(minikube service demo --url)
$ while do curl -s -w " -- `date` -- %{http_code}\n" $DEMO_URL; sleep 1s; done
```

```sh
$ kubectl apply -f demo-manifest/1-1.default.deploy.yml && kubectl get pods -w
deployment "demo" configured
NAME                    READY     STATUS              RESTARTS   AGE
demo-7fb4df47ff-zphkn   0/1       ContainerCreating   0          1s
demo-7ff7477c9b-jf26z   1/1       Terminating         0          1m
demo-7fb4df47ff-zphkn   1/1       Running   0         1s
demo-7ff7477c9b-jf26z   0/1       Terminating   0         1m
demo-7ff7477c9b-jf26z   0/1       Terminating   0         1m
demo-7ff7477c9b-jf26z   0/1       Terminating   0         1m
```

![healthcheck-v10-v11.gif](https://qiita-image-store.s3.amazonaws.com/0/2826/85569720-587e-8ffd-85ab-c97d42bca285.gif)

新しいPodがRunningになる前に、古いPodのTerminatingが実行されてしまっているのがわかります。これは、`maxUnavailable`のデフォルト値が1のため、レプリカ数が1の場合はRunning Podが0の状態を許容することになってしまうためです。

Podの状態を3つに分けて、図解するとこのようになります。

![rolling-update-v10-v11.png](https://qiita-image-store.s3.amazonaws.com/0/2826/603e260b-1998-6694-4822-7ecc8f8fe161.png)

期待値としては、新しいPod（v1.1）がRunningになってから、古いPod（v1.0）がTerminatingになることです。解決策は2つあり、maxUnavailableを0にする、もしくは、レプリカ数を増やすです。解説を単純にするために、今回はmaxUnavailableを0にします。

### Rolling updateのStrategyを変更する

```
$ diff "demo-manifest/1-1.default.deploy.yml" "demo-manifest/1-2.strategy.deploy.yml"
11a12,15
>   strategy:
>     rollingUpdate:
>       maxSurge: 1
>       maxUnavailable: 0
17c21
<         appVersion: v1.1
---
>         appVersion: v1.2
```

```sh
$ kubectl apply -f demo-manifest/1-2.strategy.deploy.yml && kubectl get pods -w
deployment "demo" configured
NAME                    READY     STATUS              RESTARTS   AGE
demo-5b47cd65cd-zg49d   0/1       ContainerCreating   0          0s
demo-7fb4df47ff-hb8xx   1/1       Running             0          13m
demo-5b47cd65cd-zg49d   1/1       Running   0         1s
demo-7fb4df47ff-hb8xx   1/1       Terminating   0         13m
demo-7fb4df47ff-hb8xx   0/1       Terminating   0         13m
demo-7fb4df47ff-hb8xx   0/1       Terminating   0         13m
demo-7fb4df47ff-hb8xx   0/1       Terminating   0         13m
```

![healthcheck-v11-v12.gif](https://qiita-image-store.s3.amazonaws.com/0/2826/79a4d325-ed74-4c6a-a7fb-cd3ab1d87f9a.gif)

一見、期待通りにRolling updateされたように見えますが、実際はユーザのリクエストを取りこぼしています。。

![rolling-update-v11-v12.png](https://qiita-image-store.s3.amazonaws.com/0/2826/0d844191-74b4-cb93-0830-5c8f541d7791.png)

これは、PodがReadyだからと言ってアプリがReadyとは限らない、という落とし穴です。アプリがReadyの状態を図に追加したのが下の図です。

![rolling-update-v11-v12-w-ready.png](https://qiita-image-store.s3.amazonaws.com/0/2826/23414302-ae67-b61a-f1f3-90f1cc2f2d1c.png)

この問題を解決するためには、アプリがReadyの状態とは何か？をKubernetesに明示的に伝える必要があります。

### アプリがReadyの状態を定義する

アプリがReadyの状態は、`ReadinessProbe`という項目で定義可能です。このdemoアプリはヘルスチェック用のエンドポイントを持っているので、「ヘルスチェックのエンドポイントへGetリクエストを投げて200 OKが返ってきた」という状態をReadyと定義するのが最も簡単な設定です。その他の設定方法は、[公式ドキュメント](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)をご参照ください。

```
$ diff "demo-manifest/1-2.strategy.deploy.yml" "demo-manifest/1-3.readinessprobe.deploy.yml"
21c21
<         appVersion: v1.2
---
>         appVersion: v1.3
31a32,35
>         readinessProbe:
>           httpGet:
>             path: /actuator/health
>             port: 8080
```

```sh
$ kubectl apply -f demo-manifest/1-3.readinessprobe.deploy.yml && kubectl get pods -w
deployment "demo" configured
NAME                    READY     STATUS              RESTARTS   AGE
demo-5b47cd65cd-zg49d   1/1       Running             0          6m
demo-c88bcc897-h5dkg    0/1       ContainerCreating   0          1s
demo-c88bcc897-h5dkg   0/1       Running   0         2s
demo-c88bcc897-h5dkg   1/1       Running   0         17s
demo-5b47cd65cd-zg49d   1/1       Terminating   0         7m
demo-5b47cd65cd-zg49d   0/1       Terminating   0         7m
demo-5b47cd65cd-zg49d   0/1       Terminating   0         7m
demo-5b47cd65cd-zg49d   0/1       Terminating   0         7m
```

![healthcheck-v12-v13.gif](https://qiita-image-store.s3.amazonaws.com/0/2826/5d350ffc-d914-ecd6-cfd0-cd8d5b4b204d.gif)

ようやく、ユーザ影響を気にせずデプロイできるようになりました。

![rolling-update-v12-v13.png](https://qiita-image-store.s3.amazonaws.com/0/2826/828e9351-d795-c81f-57f9-2698a423e01e.png)

面倒に思えますが、一回設定すれば後はKubernetesが良しなにやってくれるので、「このエラーはリリースによるものですので、無視してください。」等のコメントをしている暇があったら、是非やっておきましょう。