背景
リリースの度にユーザのリクエストを取りこぼすのは、単純な設定漏れですので図を交えながらどういった設定が必要か解説していきます。
https://github.com/TakiTake/kubernetes-rolling-update-safely
デモアプリのコードや、Kubernetesの設定ファイルは上記のリポジトリに上げてあります。
実行環境
minikube version: v0.28.2
事前準備
# 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が動けば成功です。
$ 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も作成しましょう。
$ 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/ となります。もしくは、minikube service demo
と打てば、自動でページが開きます。
次に、Rolling updateで新しいバージョンのアプリをデプロイするために、バージョンだけ更新したYAMLファイルをデプロイします。
$ diff "demo-manifest/1-0.default.deploy.yml" "demo-manifest/1-1.default.deploy.yml"
17c17
< appVersion: v1.0
---
> appVersion: v1.1
別ターミナルで、リクエストを投げ続けることで問題なくアップデートが行えたかどうか確認可能です。
# 1秒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
$ 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
新しいPodがRunningになる前に、古いPodのTerminatingが実行されてしまっているのがわかります。これは、maxUnavailable
のデフォルト値が1のため、レプリカ数が1の場合はRunning Podが0の状態を許容することになってしまうためです。
Podの状態を3つに分けて、図解するとこのようになります。
期待値としては、新しい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
$ 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
一見、期待通りにRolling updateされたように見えますが、実際はユーザのリクエストを取りこぼしています。。
これは、PodがReadyだからと言ってアプリがReadyとは限らない、という落とし穴です。アプリがReadyの状態を図に追加したのが下の図です。
この問題を解決するためには、アプリがReadyの状態とは何か?をKubernetesに明示的に伝える必要があります。
アプリがReadyの状態を定義する
アプリがReadyの状態は、ReadinessProbe
という項目で定義可能です。このdemoアプリはヘルスチェック用のエンドポイントを持っているので、「ヘルスチェックのエンドポイントへGetリクエストを投げて200 OKが返ってきた」という状態をReadyと定義するのが最も簡単な設定です。その他の設定方法は、公式ドキュメントをご参照ください。
$ 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
$ 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
ようやく、ユーザ影響を気にせずデプロイできるようになりました。
面倒に思えますが、一回設定すれば後はKubernetesが良しなにやってくれるので、「このエラーはリリースによるものですので、無視してください。」等のコメントをしている暇があったら、是非やっておきましょう。